Claude API Server-Sent Events: A Streaming Tutorial
If you're building a chat UI or any interface that shows Claude's response as it's generated, you need server-sent events (SSE). This tutorial walks through exactly how Claude API streaming works under the hood: the event types you'll receive, how to parse them correctly, and common mistakes that break streaming in production.
Server-sent events are a one-way protocol: the server keeps an HTTP connection open and pushes a sequence of small text-based messages until the response is done. For an LLM API, this means you get tokens as they're generated instead of waiting for the full completion. No WebSocket handshake, no extra libraries required — just fetch or curl and a bit of parsing logic.
How Claude's SSE stream is structured
When you set "stream": true in a request, the response Content-Type becomes text/event-stream. Each message in the stream looks like this:
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
Two lines per event: an event: line naming the event type, and a data: line with a JSON payload, followed by a blank line as the separator. You'll typically see this sequence over the life of one response:
message_start— the response object is created, includes the model and initial usage infocontent_block_start— a new content block begins (text, or a tool_use block)content_block_delta— the actual incremental text or tool input, sent repeatedlycontent_block_stop— that block is finishedmessage_delta— final metadata likestop_reasonand updated token usagemessage_stop— the stream is complete
If something goes wrong mid-stream, you may also get an error event with a JSON body describing the failure (overloaded model, invalid request, etc.) — always handle this case, not just the happy path.
Streaming with curl
curl handles SSE natively since it just reads the raw HTTP body. This is the fastest way to see the raw event stream without writing any parsing code:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku about rivers"}]
}'
You'll see the raw event:/data: pairs print to your terminal as they arrive. This is useful for debugging before you write any client code — if curl shows garbled or buffered output, the problem is in your network layer (a proxy buffering responses), not your parsing logic.
Parsing SSE in JavaScript
Browsers have EventSource, but it only supports GET requests and can't send custom headers or a JSON body — both of which you need for an LLM API call. The practical approach is to use fetch with a readable stream and parse the SSE format yourself:
const response = 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-5",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Explain SSE in two sentences" }],
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete line for next chunk
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const payload = JSON.parse(line.slice(6));
if (payload.type === "content_block_delta") {
process.stdout.write(payload.delta.text ?? "");
}
if (payload.type === "message_stop") {
console.log("\n[stream complete]");
}
}
}
The key detail most tutorials get wrong: a chunk from reader.read() doesn't necessarily align with a complete SSE message. You must buffer partial lines and only process a line once it's complete — otherwise JSON.parse will throw on truncated data.
Streaming through a gateway
If your app calls Claude through an intermediary — a proxy, a billing layer, or an internal gateway — that intermediary needs to pass the stream through without buffering the entire response first. A common bug is a backend that accumulates the full SSE stream server-side and only forwards it once complete, which defeats the purpose of streaming and adds latency for no benefit.
This matters more than it sounds like. If you're issuing per-application API keys on top of Claude access — for a multi-tenant product, a team dashboard, or usage tracking — the proxy layer has to stream chunk-by-chunk as it receives them, not after the fact. SubToAPI's streaming endpoint does exactly this: it relays Claude's SSE events through /v1/messages with "stream": true as they arrive, so you can issue sub_live_... keys per application while keeping the same chunked response behavior your frontend already expects. The streaming docs cover the exact event sequence and show both curl and JS clients, and the quickstart gets a key issued in a couple of minutes if you want to test it against your own client code.
Common SSE mistakes to avoid
- Treating
content_block_deltaas the only event — you still needmessage_deltafor finalstop_reasonand token counts, since those aren't in the delta events. - Not handling multiple content blocks — if Claude uses tools, you'll get interleaved
content_block_start/delta/stopevents per block, distinguished byindex. - Buffering the whole response server-side before forwarding it to the client, which eliminates the latency benefit of streaming entirely.
- Ignoring
errorevents mid-stream — a stream can start successfully and still fail partway through. - Not setting a read timeout — a stalled connection should be detected and retried, not left hanging indefinitely.
questions
What's the difference between SSE and WebSockets for streaming Claude responses? SSE is one-directional (server to client) over a single HTTP request, which fits the request/response shape of an LLM call. WebSockets are bidirectional and add connection management overhead you don't need for streaming a single completion.
Can I use the browser's native EventSource API with Claude? Not directly — EventSource only supports GET requests without custom headers, but Claude's API requires a POST with your API key in the headers and a JSON body. Use fetch with a readable stream instead, as shown above.
Why does my stream show garbled or delayed chunks? This is almost always a buffering issue in a proxy, CDN, or load balancer sitting between your client and the API, not the API itself. Disable response buffering on any intermediary, or test directly against the API with curl to isolate the cause.