Claude Streaming Response: A Developer's Implementation Guide
A Claude streaming response delivers the model's output incrementally, as a sequence of small events sent over an HTTP connection, instead of making you wait for the entire completion to finish before you see anything. This matters for any product where perceived latency affects usability — chat interfaces, coding assistants, voice agents — because users start reading the first words within a few hundred milliseconds rather than waiting several seconds for a long answer to complete.
Technically, streaming works through server-sent events (SSE). When you set "stream": true in a request, the API keeps the HTTP connection open and pushes a series of event:/data: pairs as the model generates tokens. Your client reads this stream line by line, reassembles the text deltas, and renders them as they arrive. This article focuses on the practical side: what the event types actually contain, how to parse them correctly, and the mistakes that break streaming in production.
What a streaming response actually contains
A typical Claude streaming session emits a fixed sequence of event types:
message_start— metadata about the message object, including the model and initial usage counterscontent_block_start— marks the beginning of a content block (text or tool use)content_block_delta— the actual incremental content, sent repeatedly as tokens are producedcontent_block_stop— closes the current content blockmessage_delta— carries updated stop reason and final usage/token countsmessage_stop— signals the end of the stream
Each content_block_delta event contains a small JSON payload with a text field (for text blocks) or a partial_json field (for tool use blocks being streamed). Your parsing logic needs to accumulate these fragments in order — they are not guaranteed to arrive as whole words or complete JSON objects.
Parsing the stream correctly
The most common bug is treating each SSE data: line as a complete, independent message. In reality, deltas are fragments that must be concatenated. Here's a minimal parser pattern in JavaScript:
const response = 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",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Explain event loops in Node.js" }]
})
});
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 ?? "");
}
}
}
Two details matter here: you must buffer partial lines across chunk boundaries (network reads don't align with SSE message boundaries), and you must check the type field before assuming a text property exists, since delta payloads differ between text blocks and tool-use blocks.
Handling tool use inside a stream
When Claude calls a tool mid-response, the stream interleaves a content_block_start with type: "tool_use", followed by content_block_delta events carrying partial_json fragments that build up the tool's input object incrementally. You need to accumulate those fragments and JSON.parse the result only once the block closes with content_block_stop — parsing partial JSON mid-stream will throw. If your application both streams text to a UI and executes tool calls, keep separate accumulators per content block index rather than a single global buffer.
Error handling and reconnection
Streaming connections can drop mid-response due to proxies, load balancers, or client-side network changes. A few practical rules:
- Always check for an
errorevent type before assuming the stream completed successfully — a stream can start normally and still fail partway through. - Don't naively retry from scratch on every reconnect; if you've already rendered partial output, decide whether your UX shows a "regenerating" state or resumes with a fresh request.
- Set a reasonable client-side timeout for the time between events, not just total request time — a stalled stream with no error event is a common failure mode with reverse proxies that don't handle SSE keep-alives well.
- Log the
message_deltaevent'sstop_reason(end_turn,max_tokens,stop_sequence, ortool_use) so you can distinguish a normal completion from a truncated one.
Streaming vs. non-streaming: when to use which
Streaming is the right default for anything user-facing and conversational. It's usually the wrong choice for batch jobs, background summarization, or any pipeline where you need the complete, validated output before doing something else with it — buffering a full stream just to reassemble it defeats the purpose and adds parsing complexity for no UX benefit.
If you're building on top of Claude through SubToAPI, streaming works the same way over the standard /v1/messages endpoint — you get the same SSE event structure, plus usage metadata per request visible in your dashboard, so you can track token consumption across streamed and non-streamed calls without instrumenting it yourself. See the streaming docs and the messages API reference for the full event schema, or the quickstart if you're setting up your first sub_live_ key.
Practical checklist before shipping
- Buffer incomplete SSE lines across chunk reads
- Accumulate
partial_jsonper content block, not globally - Handle the
errorevent type explicitly - Read
stop_reasonfrommessage_deltato detect truncation - Add a stall timeout, not just a total request timeout
- Test behind your actual production proxy/CDN, since some strip or buffer SSE by default
Questions
Does streaming change how tokens are billed? No. Token usage is calculated the same way whether a response is streamed or returned in full — streaming only changes delivery timing, not the number of input or output tokens counted.
Can I cancel a stream partway through? Yes. Closing the underlying HTTP connection (e.g., calling .abort() on the fetch controller) stops generation on the client side. Depending on the provider, tokens generated up to that point may still be counted.
Why does my proxy or CDN break streaming? Many reverse proxies buffer responses by default, which delays or batches SSE chunks instead of forwarding them immediately. You typically need to disable response buffering for the streaming endpoint specifically (e.g., X-Accel-Buffering: no on Nginx).