Claude API Streaming Response Implementation Guide
Streaming lets your application display Claude's response token by token instead of waiting for the entire completion to finish. This guide shows exactly how to implement it: setting the right request parameters, parsing server-sent events (SSE), handling partial JSON for tool use, and avoiding the most common bugs that break streaming in production.
If you're here because your UI freezes during long responses, or because you're building a chat interface and want that familiar "typing" effect, streaming is the fix. It's not complicated once you understand the event format Claude sends back — the hard part is usually on the client side: buffering chunks correctly, handling disconnects, and not blocking your event loop.
How Claude API Streaming Works
Instead of returning one JSON blob after generation completes, a streaming request keeps the HTTP connection open and sends a sequence of events as text/event-stream. Each event is a small JSON payload prefixed with event: and data: lines. The client reads these incrementally and appends text as it arrives.
The event types you'll see in a typical streaming response:
message_start— initial message metadata (id, model, role)content_block_start— a new content block begins (text or tool_use)content_block_delta— incremental text or partial JSON chunkscontent_block_stop— the current block is completemessage_delta— usage and stop_reason updatesmessage_stop— the stream is finished
You enable streaming by setting "stream": true in your request body. Everything else about the request — model, messages, system prompt, max_tokens — stays the same.
Basic Implementation with curl
For debugging, curl is the fastest way to see raw SSE output:
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": "Explain event loops in Node.js"}]
}'
You'll see a stream of event: / data: pairs scroll by in the terminal. This is useful for confirming your prompt and parameters work before writing client code.
Implementing Streaming in JavaScript
In a browser or Node.js environment, you typically use fetch with a readable stream reader rather than the EventSource API, since EventSource doesn't support custom headers or POST bodies.
async function streamMessage(prompt) {
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: prompt }]
})
});
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 = line.slice(5).trim();
if (!payload) continue;
const event = JSON.parse(payload);
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text ?? "");
}
}
}
}
The key detail most implementations get wrong: SSE chunks don't align with JSON message boundaries. A single read() call might return half an event, or several events at once. You must buffer incomplete lines and only parse complete ones — that's what the buffer.split("\n") / buffer.pop() pattern handles above.
Handling Tool Use in Streams
When Claude calls a tool mid-stream, the JSON arguments arrive as partial fragments across multiple content_block_delta events with type: "input_json_delta". You need to accumulate these fragments into a string and only JSON.parse() once the block closes (content_block_stop). Parsing partial JSON on every delta will throw errors — wait for the full accumulated string.
let toolInputJson = "";
if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
toolInputJson += event.delta.partial_json;
}
if (event.type === "content_block_stop") {
const toolInput = JSON.parse(toolInputJson);
// execute tool with toolInput
}
Error Handling and Reconnection
Streaming connections can drop mid-response due to network issues or timeouts. A production implementation needs to:
- Set a reasonable client-side timeout that's longer than your expected generation time
- Catch stream errors and surface a clear retry option to the user rather than silently truncating output
- Track
message_deltaevents forstop_reason— if it'smax_tokens, the response was cut off intentionally, not due to an error - Avoid retrying by simply resending the same request if partial output was already shown — that duplicates content; instead, discard and restart cleanly
Streaming via SubToAPI
If you're already calling the Claude API through your own backend, SubToAPI gives you the same streaming behavior over a stable HTTPS endpoint with an sub_live_... application key, so you don't have to manage provider-specific SDKs or rotate raw provider credentials across services. The request shape is nearly identical:
curl 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": "Summarize this changelog"}]
}'
This is useful if you're issuing separate API keys per app or per team seat and want usage metadata on each streamed request without building that tracking yourself. See the streaming docs and messages reference for the full event schema, or the quickstart to get a key from signup.
Common Pitfalls
- Parsing JSON per chunk instead of per line. Always split on newlines and parse only complete
data:lines. - Ignoring
message_deltausage info. This is where final token counts and stop reason live — don't assume they're inmessage_stop. - Blocking the event loop in Node.js. If you're writing streamed text to a database or socket, make sure those writes are async so they don't stall the reader.
- Not handling
pingevents. Anthropic sends periodic keep-alive pings; your parser should silently ignore unknown event types rather than throwing.
Questions
Does streaming cost more than non-streaming requests? No. Token usage and pricing are identical — streaming only changes how the response is delivered, not how it's billed.
Can I use the EventSource browser API instead of fetch? Not directly, because EventSource only supports GET requests without custom headers, and Claude API calls require POST with an API key header. Use fetch with a stream reader instead.
What happens if my client disconnects mid-stream? The server-side generation typically continues until completion or token limit, but any tokens generated after disconnect are lost to that client. Your application should detect the disconnect and offer the user a retry rather than attempting to resume the same stream.