Claude API Streaming Response: SSE Explained
A streaming response from the Claude API sends the model's output token by token over a persistent HTTP connection instead of waiting for the full answer to be generated before returning anything. You set "stream": true in your request body, keep the connection open, and read a sequence of server-sent events as they arrive. This is what makes chat UIs feel instant — text appears while the model is still "typing" instead of the user staring at a spinner for 10+ seconds.
The short answer to "how do I get a streaming response from the Claude API": send your request with stream: true, read the response body as a stream of Server-Sent Events (SSE), and accumulate the text_delta fields from each content_block_delta event until you receive a message_stop event. The rest of this article covers the mechanics, the event types you'll actually see, and how to parse them without missing chunks or leaking connections.
Why Stream at All
Non-streaming requests are simpler to code but have two real costs:
- Perceived latency. A 500-token response might take 8-15 seconds to generate. Without streaming, the user sees nothing until it's completely done.
- No early exit. If you're building something that needs to react to output as it's produced (live translation, code generation previews, voice synthesis), you need tokens as they're generated, not after.
Streaming doesn't make the model faster — total generation time is roughly the same — but it makes the first byte arrive in a few hundred milliseconds, which is what users actually perceive as speed.
The Event Format
Claude's streaming API returns a sequence of SSE events, each with a type field. The ones you'll handle in practice:
message_start— the message object is created, includes model and initial usagecontent_block_start— a new content block (text or tool use) beginscontent_block_delta— the actual incremental content, e.g.{"type": "text_delta", "text": "Hello"}content_block_stop— a content block is finishedmessage_delta— updates to top-level fields likestop_reasonand final usagemessage_stop— the stream is done
Each event is sent as plain text in the standard SSE format:
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}
Your job is to parse these lines, extract the data: payload, JSON-decode it, and react based on type.
Streaming with curl
For debugging, curl with -N (no buffering) is the fastest way to see raw events:
curl -N 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 caching."}]
}'
You'll see events scroll by in real time. This is worth doing once even if you're building in JavaScript or Python — it makes the abstract "stream of events" concept concrete before you write a parser.
Streaming with JavaScript
In Node or the browser, fetch gives you a ReadableStream you can read chunk by chunk. Here's a minimal SSE parser that handles partial lines correctly (a common bug is assuming each chunk is a complete event, which it often isn't):
async function streamMessage(payload) {
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({ ...payload, stream: true }),
});
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(); // last line may be incomplete
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text);
}
if (event.type === "message_stop") {
return;
}
}
}
}
The key detail: keep a buffer and only process complete lines. Network chunks don't align with SSE event boundaries, so splitting naively on newlines without buffering the tail will randomly corrupt JSON parsing under load.
Handling Errors Mid-Stream
Streaming introduces a failure mode non-streaming requests don't have: the connection can drop after you've already received partial output. Design for this explicitly:
- Buffer partial text so you can decide whether to discard it, retry the whole request, or show it with a "response interrupted" note.
- Watch for an
errorevent type, which Claude sends if something goes wrong mid-generation (e.g. an overload condition). - Set a reasonable client-side timeout — if no bytes arrive for 30+ seconds, treat it as a stalled connection and retry rather than hanging indefinitely.
Streaming Through SubToAPI
If you're already using SubToAPI to turn your Claude access into an HTTPS API, streaming works the same way: set stream: true, point requests at https://api.subtoapi.app/v1/messages, and authenticate with your sub_live_... key instead of an Anthropic key.
curl -N https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku about caching."}]
}'
The event format and parsing logic above apply unchanged — SubToAPI passes through the same SSE structure so existing client code doesn't need modification. See /docs/streaming for the full event reference, and /docs/quickstart if you're setting this up for the first time. Usage metadata (input/output tokens) is still returned in the final message_delta event, which matters if you're tracking per-request cost across a team — something worth checking against the pricing page if you're issuing separate keys per application.
FAQ
Does streaming reduce total response time? No. Total generation time is roughly the same as a non-streaming request. What streaming reduces is time-to-first-byte — the user sees output start almost immediately instead of waiting for the entire response to finish generating.
Can I stream tool use responses? Yes. Tool calls arrive as content_block_start/content_block_delta/content_block_stop events with type: "tool_use", with the input JSON built up incrementally across deltas. See /docs/tools for how tool-use content blocks are structured.
What happens if the connection drops mid-stream? You keep whatever partial text you've already buffered client-side, but the request itself is not automatically resumed — you need to retry with the original prompt (or a continuation) if the full response is required.