---
title: "How it works"
description: "What happens between a request and a rendered page."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.rsc-kit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# How it works

Three pieces, and each one can be replaced without disturbing the others.

- **The plugin** reads your route tree at build time and generates the entries.
- **The engine** turns a component and its layouts into a Flight payload and an HTML stream.
- **The host** owns HTTP. It matches a URL, decides how much of the page to send, and streams the answer.

The generated Nitro entry is a host, and it knows nothing about your routes
that the manifest does not tell it.

> **What a Flight payload is**
>
> React's own wire format for a rendered tree — the server's output, serialised.
> It is not HTML: it describes elements, which client components to load and the
> props to give them, so the browser can rebuild the same tree and attach to it.
>
> The server renders a page twice over: once to a Flight payload, then that
> payload to HTML. A navigation asks for the payload alone, because the browser
> can render it into the page it already has.

## A first load

```text
GET /posts/hello
  → the host matches the route in the manifest
  → the engine renders the page inside its layout chain
  → HTML streams out: shell first, Suspense completions as they resolve
  → the Flight payload is embedded for hydration
  → the client bundle hydrates, and the page becomes an SPA
```

The shell — layouts, static markup, every Suspense fallback — is flushed
before anything slow is awaited, so the browser has something to paint while
the data is still in flight.

## A navigation

A `<Link>` click does not ask for the document again. It sends the layout chain
it already has, and gets back only what is below the deepest layout they share:

```text
GET /posts/world
  X-RSC: true
  X-RSC-Segments: app/layout,app/posts/layout

  ← X-RSC-Segment-Depth: 2
  ← X-RSC-Layouts: app/layout,app/posts/layout
  ← a Flight payload for the page alone
```

Depth `0` means a whole document: it replaces the root and clears everything
under it. Anything deeper is handed to a segment boundary — a small client
component sitting between each layout and its children.

It has to be a client component, because server components cannot re-render in
the browser.

The engine gets the last word on the depth: an interceptor targeting a slot on
a layout the client is keeping forces a wider render, because an override can
never reach a layout that is not being re-rendered.

## The headers

| Header | Direction | Meaning |
| --- | --- | --- |
| `X-RSC` | → | Send the Flight payload, not the page. Every answer varies on it. |
| `X-RSC-Segments` | → | The layout chain the client has mounted, outermost first. |
| `X-RSC-Segment-Depth` | ← | The boundary this payload replaces. `0` is a whole document. |
| `X-RSC-Layouts` | ← | The chain to send back next time. |
| `X-RSC-Version` | ↔ | The build. A mismatch mid-session answers `409`. |
| `X-RSC-Action` | → | The server reference being invoked. |
| `X-RSC-Content-Type` | → | The body's real type, because the body itself is sent opaque. |
| `X-RSC-Referer` | → | The page an action or interception came from. |
| `X-RSC-Intercept` | → | Render this route into the named slot. |
| `X-RSC-Revalidate` | → | Render this one region and nothing else. |
| `X-RSC-Location` | ← | Go here instead. |
| `X-RSC-Redirect` | ← | The render redirected. Sent with `204` on a navigation, never a 3xx — `fetch` would follow that and hand the decoder the wrong body. |

## A server action

One `POST /_rsc/action`, carrying the reference id and the arguments in Flight
format. The body travels as `application/octet-stream` with its real type in a
header, so a host that parses multipart cannot consume it before the action
does — reading `Content-Type` instead leaves an upload undecodable.

Anything the action marks stale with `revalidate()` is re-rendered and travels
back **inside the same response**. The caller sees only what its function
returned; the page updates around it, and there is no second request.

A failed action answers with JSON or a redirect header, never a Flight stream.
Handing a failure to the Flight decoder reports the decoder's confusion rather
than what the server said.

## A redirect from a render

Headers flush before the render's slow work, so a redirect has two ways to
arrive and which one depends on where it was thrown:

- **Above every Suspense boundary.** React cannot finish the shell, so the promise for it rejects while the response is still unwritten. A document gets a real 3xx; a navigation gets `204` and `X-RSC-Redirect`.
- **Inside a boundary.** The shell is already out. The destination travels in React's error digest for the client's boundary to perform, and a document also gets a `location.replace` script appended so it need not hydrate first.

Neither buffers anything. See [Redirects](/guides/redirects) for what that
means for an authorization check.

## Frozen answers

Before matching a route, the host looks for a prerendered file. A frozen page
is a whole page, so that lookup happens *after* the two things that ask for
something smaller — a revalidation and an interception — and before everything
else.

Prerendered routes answer partially too: alongside `{path}.flight` the build
writes one variant per depth, and the host serves the one matching the chain
this client shares. Without them, arriving at a stored page would always
replace the document root and unmount the pages retained behind it.

A route that only redirects is stored as the redirect — `{path}.redirect.json`,
holding a status and a location — and answered from that file without rendering
anything.

## Deployments

The version hash identifies the build. A client that navigates after a deploy
gets a `409` and `X-RSC-Location`, which is a full reload — a layout retained
from the previous build has no claim on being right for the new one.

## Further

[`PROTOCOL.md`](https://github.com/rsc-kit/rsc-kit/blob/main/PROTOCOL.md)
in the repository is the full contract: the engine's interface, the socket
framing a detached host uses, and the invariants that fail silently when a new
host gets them wrong. `tests/js/workerProtocol.test.ts` is its executable half.

Source: https://docs.rsc-kit.dev/reference/how-it-works/index.mdx
