Skip to content

React Server Components Wire Format

advanced16 min read

What Problem Does the Flight Protocol Solve?

React Server Components run on the server and send their output to the client. But they can't just send HTML — they need to send something richer. HTML loses the React component tree structure. It can't express "this is a Server Component that rendered a Client Component with these props." It can't stream partial results. It can't send references to client-side JavaScript modules.

The Flight protocol is React's custom wire format for serializing the RSC tree into a streamable, type-safe format that preserves component boundaries, client component references, and data payloads.

Traditional SSR:
  Server → HTML string → Client parses HTML → Hydrates with JS

RSC with Flight:
  Server → Flight payload (streaming) → Client reconstructs React tree → Renders

The Mental Model

Mental Model

Think of the Flight protocol like a blueprint with assembly instructions being sent from a factory (server) to a construction site (client).

Traditional SSR sends the finished building as a photo (HTML). The construction site has to reverse-engineer the photo to figure out where the electrical wiring goes (hydration).

The Flight protocol sends actual blueprints: "Here's a wall (div). Inside it, put a cabinet (Server Component output, already built). Next to it, plug in this specific appliance (Client Component reference at ./components/Counter.js), and here are its settings (props: {initial: 5})."

The construction site follows the blueprint exactly — no guesswork, no reverse-engineering. It knows which parts are pre-built (Server Component output) and which parts need to be assembled on-site (Client Components).

The Flight Payload Format

When you navigate to an RSC-powered page, the response is not HTML — it's a stream of Flight payload lines. Each line starts with an ID and a type marker.

Let's trace a simple page:

// Server Component
export default function ProductPage({ params }) {
  const product = getProduct(params.id)

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  )
}

// Client Component
'use client'
export function AddToCartButton({ productId, price }) {
  const [added, setAdded] = useState(false)
  return (
    <button onClick={() => { addToCart(productId); setAdded(true) }}>
      {added ? 'Added!' : `Add to Cart - $${price}`}
    </button>
  )
}

The Flight payload for this page looks like:

0:["$","div",null,{"children":[
  ["$","h1",null,{"children":"Wireless Headphones"}],
  ["$","p",null,{"children":"Premium noise-canceling headphones"}],
  ["$","$L1",null,{"productId":42,"price":299}]
]}]
1:I["./components/AddToCartButton.js","AddToCartButton"]

Let's decode each part:

Line 0: The rendered React tree
  "$"        → React element marker
  "div"      → HTML element type
  null       → key (none)
  {children} → child elements

  "$L1"      → "Lazy reference to module chunk 1"
               This is a Client Component placeholder

Line 1: Module reference for chunk 1
  "I"        → Module import instruction
  Path       → JavaScript module to load
  Export     → Named export to use

The client reads this payload, builds the React tree, encounters $L1, downloads AddToCartButton.js, renders the Client Component with the serialized props {productId: 42, price: 299}.

Quiz
In a Flight payload, you see $L3 in the React tree followed by line 3:I[./components/Chart.js,Chart]. What does this tell the client?

Streaming Flight Payloads

The Flight protocol is designed for streaming. The server doesn't wait for all data to be ready — it sends chunks as they resolve.

export default async function Dashboard() {
  const user = await getUser()  // Fast: 20ms

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <Suspense fallback={<p>Loading stats...</p>}>
        <Stats />  {/* Slow: 500ms */}
      </Suspense>
    </div>
  )
}

async function Stats() {
  const stats = await getStats()
  return <StatsGrid data={stats} />
}

The Flight payload streams in two chunks:

// Chunk 1 (sent at ~25ms):
0:["$","div",null,{"children":[
  ["$","h1",null,{"children":"Welcome, Sarah"}],
  ["$","$Sreact.suspense",null,{
    "fallback":["$","p",null,{"children":"Loading stats..."}],
    "children":"$L2"
  }]
]}]

// Chunk 2 (sent at ~525ms, when stats data resolves):
2:["$","div",null,{"className":"stats-grid","children":[
  ["$","div",null,{"children":"Revenue: $45,200"}],
  ["$","div",null,{"children":"Users: 12,340"}]
]}]

The client receives Chunk 1 immediately, renders the welcome message and the loading fallback. When Chunk 2 arrives 500ms later, React replaces the Suspense fallback with the resolved Stats content — no page refresh, no client-side fetch needed.

Flight payload type markers

Each line in the Flight payload starts with an ID and a type marker:

ID:TYPE[DATA]

Type markers:
  (none)  → React element tree (the default)
  I       → Module import reference (Client Component)
  S       → Symbol (like react.suspense, react.fragment)
  HL      → Hint for preloading resources
  T       → Text content
  E       → Error

Some real examples:

0:["$","div",null,{"children":"Hello"}]     → React element
1:I["./Counter.js","Counter"]               → Client module reference
2:HL["/_next/static/css/app.css","style"]   → Preload hint for CSS
3:E{"message":"Data fetch failed","stack":...}  → Error boundary trigger

The HL (hint) lines are particularly clever. Before the React tree even finishes streaming, the server can send hints to the browser to start preloading CSS, fonts, or JavaScript that it knows will be needed. This parallelizes resource loading with the Flight stream.

How Data Flows Through RSC

Understanding the data flow is critical for building efficient RSC applications:

┌─────────────────────────────────────────┐
│                 SERVER                   │
│                                          │
│  ┌────────────────────┐                  │
│  │  Server Component   │                  │
│  │  (runs on server)   │                  │
│  │                     │                  │
│  │  - Fetches data     │                  │
│  │  - Reads database   │                  │
│  │  - Access secrets   │                  │
│  │                     │                  │
│  │  Outputs:           │                  │
│  │  - HTML elements    │                  │
│  │  - Serialized props │                  │
│  │  - Client Component │                  │
│  │    references       │                  │
│  └────────┬───────────┘                  │
│           │                              │
│    Flight Protocol                       │
│    (streaming serialization)             │
│           │                              │
└───────────┼──────────────────────────────┘
            │
            ▼
┌─────────────────────────────────────────┐
│                 CLIENT                   │
│                                          │
│  Flight Decoder reads payload:           │
│  - Renders HTML elements directly        │
│  - Downloads Client Component JS         │
│  - Hydrates Client Components with       │
│    serialized props                      │
│                                          │
│  ┌────────────────────┐                  │
│  │  Client Component   │                  │
│  │  (runs in browser)  │                  │
│  │                     │                  │
│  │  - useState/useEffect│                 │
│  │  - Event handlers   │                  │
│  │  - Browser APIs     │                  │
│  └────────────────────┘                  │
└─────────────────────────────────────────┘

What Can Be Serialized in Flight

The Flight protocol can serialize these types across the server/client boundary:

Serializable (can pass as props to Client Components):
  ✅ Strings, numbers, booleans, null, undefined
  ✅ Arrays and plain objects (containing serializable values)
  ✅ Date objects
  ✅ Map and Set
  ✅ Typed arrays (Uint8Array, etc.)
  ✅ React elements (JSX) — including Server Component output
  ✅ Promises (streamed via Suspense)

NOT serializable (cannot pass as props):
  ❌ Functions (closures capture server scope)
  ❌ Classes (non-plain objects)
  ❌ Symbols (except React-internal ones)
  ❌ DOM nodes
  ❌ Streams (Node.js streams, ReadableStream)
Quiz
You have a Server Component that fetches user data and passes it to a Client Component. Which of these props will cause a serialization error?

RSC Payload During Navigation

When you navigate between pages in a Next.js app, the browser doesn't request full HTML pages. It requests Flight payloads:

Initial page load (full HTML + Flight payload embedded):
  GET /products/42
  Response: Full HTML document with embedded Flight payload
  → Browser renders HTML immediately
  → React hydrates using the embedded Flight payload

Client-side navigation (Flight payload only):
  GET /products/43 (with RSC headers)
  Response: Just the Flight payload (no HTML wrapper)
  → React reads the Flight payload
  → Updates the component tree in place
  → No full page reload
// Request headers for RSC navigation:
RSC: 1                    // "I want the Flight payload, not HTML"
Next-Router-State-Tree: [...] // Current router state for diffing
Next-Url: /products/43    // The target URL

This is why Next.js App Router navigations feel like SPA transitions — the browser fetches only the Flight payload for the new route, and React reconciles the new tree with the current one, updating only what changed.

Caching Flight Payloads

Next.js caches Flight payloads in the client-side Router Cache:

First visit to /products/42:
  → Full render on server
  → Flight payload sent and cached in Router Cache (in memory)

Navigate away to /products/43:
  → New Flight payload fetched and cached

Navigate back to /products/42:
  → Router Cache hit — instant navigation, no server request
  → In Next.js 15, dynamic pages are not cached in the Router Cache by default (staleTimes.dynamic defaults to 0). Static pages cache for 5 minutes. These values are configurable via staleTimes in next.config.

Production Scenario: Building an RSC-Powered Dashboard

A team builds a dashboard where the main layout is a Server Component that fetches shared data, and individual widgets are a mix of Server and Client Components:

// Server Component: Dashboard layout
export default async function Dashboard() {
  const org = await getOrganization()  // Server-side, no API needed

  return (
    <div>
      <DashboardHeader org={org} />

      <div className="grid grid-cols-3 gap-4">
        {/* Server Component: data fetched on server */}
        <Suspense fallback={<CardSkeleton />}>
          <RevenueCard orgId={org.id} />
        </Suspense>

        {/* Client Component: needs interactivity for chart hover */}
        <Suspense fallback={<CardSkeleton />}>
          <InteractiveChart orgId={org.id} />
        </Suspense>

        {/* Server Component rendered, Client Component inside */}
        <Suspense fallback={<CardSkeleton />}>
          <UserList orgId={org.id} />
        </Suspense>
      </div>
    </div>
  )
}

The Flight payload for this page streams as:

Chunk 1: Layout + header (immediate)
  → DashboardHeader renders to HTML elements in the Flight payload
  → Three Suspense boundaries with fallbacks

Chunk 2: RevenueCard resolves (100ms)
  → Pure Server Component output — just HTML elements, zero client JS

Chunk 3: InteractiveChart resolves (300ms)
  → Contains a $L reference to the chart Client Component
  → Props include the pre-fetched chart data (serialized)
  → Client downloads chart JS and renders with the data

Chunk 4: UserList resolves (200ms)
  → Server-rendered user list HTML
  → Contains a $L reference to a "Load More" Client Component button

The beauty: RevenueCard ships zero JavaScript to the client. It's a Server Component that renders to plain HTML elements in the Flight payload. The data fetching, database queries, and rendering all happened on the server.

Quiz
In a Next.js App Router page, a Server Component passes a data object (fetched from the database) as a prop to a Client Component. Where does the data serialization happen?

Common Mistakes

What developers doWhat they should do
Passing functions as props from Server Components to Client Components
Functions capture server-side closure scope and cannot be serialized in the Flight protocol. If you need to trigger a server operation from a Client Component, use a Server Action (which serializes as a special reference).
Define event handlers and callbacks inside the Client Component, or use Server Actions for mutations
Creating an API route to fetch data that a Server Component could fetch directly
Server Components eliminate the API layer for data fetching. Creating /api/getData and fetching it from a Server Component adds an unnecessary network hop. Just import your database client and query directly.
Fetch data directly in the Server Component — it runs on the server and has full access to databases, secrets, and internal services
Making every component a Client Component because you are unsure about the Server/Client boundary
Every Client Component adds JavaScript to the browser bundle. Server Components ship zero JS. Keep the 'use client' boundary as deep in the tree as possible — push interactivity to leaf components.
Default to Server Components. Only add 'use client' when a component needs hooks, event handlers, or browser APIs
Assuming the Flight payload is a REST API that you can call directly
The Flight format is undocumented, unstable, and can change between React versions. Never parse it manually or build tooling on top of its structure. Use React's official APIs (Server Components, Server Actions) to interact with it.
The Flight protocol is an internal wire format between React server and client runtimes — not a public API

Key Rules

Key Rules
  1. 1The Flight protocol is React's streaming wire format for serializing Server Component trees, Client Component references, and data into a streamable payload.
  2. 2Server Components render to React elements in the Flight payload. Client Components appear as lazy module references ($L) with serialized props.
  3. 3Only serializable values can cross the server/client boundary: strings, numbers, objects, arrays, Dates, Maps, Sets. Functions and classes cannot be serialized.
  4. 4Flight payloads stream via Suspense boundaries — resolved chunks are sent as they complete, allowing progressive UI updates.
  5. 5Client-side navigation in Next.js fetches Flight payloads (not HTML), enabling SPA-like transitions without full page reloads.
  6. 6Server Components ship zero JavaScript to the client. Keep the 'use client' boundary as deep in the component tree as possible to minimize client bundle size.