Claude API Streaming with Server Components
Streaming Claude API output through React Server Components isn't as straightforward as streaming to a plain client. Server Components render on the server and serialize their output as part of the React tree — they don't naturally support token-by-token updates the way a client-side fetch with a ReadableStream does. If you want a chat interface or live-generating text UI built on the Next.js App Router, you need a specific pattern: a Route Handler that proxies the Claude API stream, and a Client Component that consumes it.
This article covers that pattern end to end: why Server Components alone can't stream tokens, how to set up a streaming Route Handler, and how to read the stream on the client without extra libraries.
Why Server Components Can't Stream Tokens Directly
A React Server Component runs once per request and its output is flushed as it resolves — it's not designed to push incremental updates to an already-rendered UI. You can stream data fetching inside RSC (via Suspense boundaries), but you can't stream individual tokens from an LLM into a component that has already been sent to the client without a client-side loop reading a live connection.
So the practical architecture looks like this:
- A Route Handler (
app/api/chat/route.ts) receives the request and calls the Claude API (or a gateway like SubToAPI) with streaming enabled. - The handler returns the response as a
ReadableStreamwith the right SSE headers. - A Client Component opens a
fetchto that route, reads the stream with aReadableStreamDefaultReader, and updates local state as chunks arrive.
Server Components are still useful here — for the initial page shell, for loading conversation history from a database, for auth checks — but the actual token streaming happens client-side against your own API route.
Setting Up the Route Handler
Here's a minimal Route Handler that proxies a streaming Claude API call:
// app/api/chat/route.ts
export const runtime = 'edge';
export async function POST(req: Request) {
const { messages } = await req.json();
const upstream = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages,
stream: true,
}),
});
return new Response(upstream.body, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
Running this on the edge runtime matters — the Node.js runtime buffers responses differently and can delay flushing chunks, which defeats the purpose of streaming. Edge functions forward bytes as they arrive.
If you're routing through SubToAPI instead of calling Anthropic directly, the shape is identical — you just swap the endpoint and auth header:
const upstream = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages,
stream: true,
}),
});
This is useful when the team members hitting this route don't each need their own Anthropic console access — the app authenticates once with a sub_live_... key and SubToAPI handles routing usage back to your Claude access. Details on the streaming format are in the streaming docs.
Consuming the Stream in a Client Component
The client side is a normal fetch with manual stream reading — no server-sent-events library required for basic cases:
'use client';
import { useState } from 'react';
export function Chat() {
const [output, setOutput] = useState('');
async function send(message: string) {
setOutput('');
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ messages: [{ role: 'user', content: message }] }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n').filter((l) => l.startsWith('data:'));
for (const line of lines) {
const data = line.replace('data: ', '');
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content_block_delta') {
setOutput((prev) => prev + parsed.delta.text);
}
} catch {
// ignore partial JSON at chunk boundaries
}
}
}
}
return (
<div>
<button onClick={() => send('Explain streaming in one sentence.')}>
Ask Claude
</button>
<p>{output}</p>
</div>
);
}
This component has to be a Client Component ('use client') — it's the only place in the App Router where you can hold mutable state and read a live stream. Everything above it in the tree (layout, page shell, auth guard) can stay as Server Components.
Handling Partial JSON Chunks
Claude's SSE stream sends content_block_delta events, but network chunks don't always align with event boundaries. A single read() call might return half an event. The TextDecoder's stream: true option handles partial UTF-8 sequences, but you still need to buffer incomplete lines across reads in production code — split on \n\n and keep any trailing incomplete segment for the next chunk rather than discarding it. The example above simplifies this for readability; add a buffer variable if you're shipping this to production.
Where SubToAPI Fits
If you're building this pattern across a team — multiple engineers each needing streaming access to Claude without managing separate Anthropic billing — SubToAPI turns your existing Claude access into application keys with the same streaming semantics shown above. You get per-key usage metadata, so you can see which route or which team member is generating the token volume, without changing the client-side reading code at all. Start with the quickstart to get a sub_live_... key, then swap the endpoint in your Route Handler.
Common Mistakes
- Using the Node.js runtime for the proxy route. It buffers output and you lose the streaming benefit. Set
runtime = 'edge'explicitly. - Trying to stream from a Server Component render. It won't work — streaming state needs a client-side reader loop.
- Forgetting
Cache-Control: no-cache. Some CDNs and browsers will buffer SSE responses without it. - Not handling stream aborts. If a user navigates away mid-stream, call
reader.cancel()in a cleanup function to avoid orphaned connections.
Questions
Can a Server Component stream Claude tokens directly to the browser without a Client Component? No. Server Components render once and serialize their output; incremental token updates require a Client Component reading a live stream, typically via a Route Handler acting as a proxy.
Does the edge runtime matter for streaming Route Handlers? Yes. The Node.js runtime in Next.js can buffer response bodies, delaying chunk delivery. Setting export const runtime = 'edge' on the Route Handler ensures bytes are forwarded as they arrive.
Can I use SubToAPI instead of calling the Anthropic API directly in the Route Handler? Yes — replace the endpoint with https://api.subtoapi.app/v1/messages and the x-api-key header with Authorization: Bearer $SUBTOAPI_KEY. The streaming response format is the same, so client-side reading code doesn't change. See /docs/streaming for details.