Claude API Streaming Responses in Next.js Edge Runtime
Streaming a Claude API response through a Next.js Edge runtime route means proxying a server-sent events (SSE) stream from Anthropic's API through your own route.ts handler, without buffering the whole response first, so the client sees tokens as they arrive. The Edge runtime supports the Web Streams API (ReadableStream, TransformStream), which is exactly what you need — but it doesn't support Node.js APIs like http.Agent or fs, so your streaming code has to be written against fetch and web streams from the start.
This article walks through a working implementation: an Edge API route that calls Claude with stream: true, parses the SSE chunks, and re-streams them to a React client using fetch with a readable body reader.
Why Edge runtime for streaming
The Edge runtime starts faster (no cold Node.js boot), stays alive per-request without holding a full Node process, and is billed differently on platforms like Vercel. For a chat UI where you want the first token to appear in under a second, that startup latency difference matters. The tradeoff: no native Node SDK support in some cases, and you're limited to Web APIs — which is fine for streaming, since fetch + ReadableStream is the native way to do it anyway.
Setting up the route
// 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: {
"content-type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY!,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 1024,
stream: true,
messages,
}),
});
if (!upstream.ok || !upstream.body) {
return new Response("Upstream error", { status: 502 });
}
return new Response(upstream.body, {
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache, no-transform",
connection: "keep-alive",
},
});
}
This is the simplest version: pass the upstream body straight through. It works because both sides speak SSE, and the Edge runtime lets you hand a ReadableStream directly to the Response constructor without buffering it in memory.
Parsing SSE events yourself
Passing the raw stream through is fine if your client also understands Claude's event format (content_block_delta, message_stop, etc.). Often you want to transform it — extracting just the text deltas before sending them to the browser. Use a TransformStream to do that on the Edge:
function parseClaudeStream() {
const decoder = new TextDecoder();
let buffer = "";
return new TransformStream({
transform(chunk, controller) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
const dataLine = line.split("\n").find((l) => l.startsWith("data:"));
if (!dataLine) continue;
const json = dataLine.replace("data:", "").trim();
if (json === "[DONE]") continue;
try {
const event = JSON.parse(json);
if (event.type === "content_block_delta" && event.delta?.text) {
controller.enqueue(new TextEncoder().encode(event.delta.text));
}
} catch {
// partial JSON, wait for more chunks
}
}
},
});
}
Pipe the upstream body through it before returning:
const transformed = upstream.body.pipeThrough(parseClaudeStream());
return new Response(transformed, {
headers: { "content-type": "text/plain; charset=utf-8" },
});
Now the client receives plain text chunks it can append directly to a message bubble — no SSE parsing needed on the frontend.
Reading the stream on the client
async function sendMessage(text: string, onChunk: (t: string) => void) {
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
onChunk(decoder.decode(value, { stream: true }));
}
}
Call onChunk to append to component state as tokens arrive. React 18's automatic batching handles frequent state updates reasonably well, but for very fast streams consider throttling UI updates to every 30–50ms instead of on every chunk.
Common Edge runtime pitfalls
- Body size limits and timeouts. Edge functions on most platforms have shorter max execution windows than serverless Node functions. Long Claude generations (many thousands of tokens) can hit that ceiling — keep
max_tokensreasonable or fall back to a Node runtime route for long-form generation. - No
process.envfallback typing. Set your Anthropic key in environment variables available to Edge functions specifically; some platforms scope Edge env vars separately from Node ones. - Buffering by proxies. If you deploy behind a CDN or reverse proxy, make sure
cache-control: no-transformis set, otherwise some proxies buffer the whole response before forwarding it, defeating the purpose of streaming. - Testing locally isn't the same as production. Local dev servers sometimes buffer differently than the Edge runtime in production. Verify actual token-by-token behavior in a preview deployment before assuming it works.
When to add a layer in front of Claude
If you're calling the Anthropic API directly from multiple Next.js apps or services, you end up rebuilding the same streaming proxy logic — SSE parsing, retry handling, usage tracking — in each one. SubToAPI sits between your app and the underlying Claude access, giving you an sub_live_... API key, the same streaming response format shown in /docs/streaming, and per-key usage metadata in a dashboard, so the Edge route above works unchanged — you just swap the endpoint and auth header. It's useful if you're distributing keys across a team or multiple projects and want centralized usage visibility without building that tooling yourself. Check /docs/quickstart for the request format, or /pricing for plan details.
questions
Does the Next.js Edge runtime fully support streaming fetch responses? Yes. The Edge runtime is built on Web Streams, so ReadableStream, TransformStream, and returning a Response with a streaming body all work natively — this is actually a better fit for streaming than the Node.js runtime's older stream APIs.
Why does my stream arrive all at once instead of token by token? Usually a proxy or CDN layer is buffering the response. Set cache-control: no-cache, no-transform on the response headers, disable response compression for that route, and confirm the behavior on an actual deployment rather than local dev.
Can I use the Anthropic Node SDK in an Edge route? Partially — recent SDK versions support fetch-based transports that work on Edge, but if you hit compatibility issues, calling the REST API directly with fetch (as shown above) is the more reliable approach for Edge deployments.