Claude Streaming Response: Implementation Guide
Streaming lets your app render Claude's answer token-by-token instead of waiting for the full response, which matters for chat UIs, coding assistants, and anything where perceived latency affects usability. Implementing it correctly means opening a persistent HTTP connection, parsing Server-Sent Events (SSE) as they arrive, and reassembling incremental deltas into a coherent message on the client.
This guide walks through the actual mechanics: the event types you'll see, how to parse them without dropping data mid-stream, how to handle tool use inside a stream, and how to deal with reconnects and errors. The examples use plain HTTP against a Claude-compatible API so you can apply the same logic whether you're calling Anthropic directly or a proxy like SubToAPI.
How Claude Streaming Works
Streaming responses use the text/event-stream content type over a single long-lived HTTP connection. Instead of one JSON blob, the server sends a sequence of named events, each carrying a small JSON payload. Your client reads the stream incrementally and appends text as it arrives.
A typical stream looks like this, in order:
message_start— the message object is created, with empty contentcontent_block_start— a new content block begins (text or tool_use)content_block_delta— repeated many times, each with a small text or JSON fragmentcontent_block_stop— the current block is completemessage_delta— top-level changes, like stop_reason and usage totalsmessage_stop— the stream is finished
Knowing this order matters because your parser needs to accumulate deltas per block, not just concatenate raw text blindly — especially once tool use enters the picture.
Making a Streaming Request
To enable streaming, set "stream": true in your request body and read the response as a stream rather than a single JSON payload.
curl -N https://api.example.com/v1/messages \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{
"model": "claude-sonnet",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Explain event loops in 3 sentences"}]
}'
The -N flag disables curl's output buffering so you see events as they come in, rather than all at once at the end.
Parsing SSE on the Client
In the browser or Node, fetch with a ReadableStream reader is the standard approach. Each SSE event is separated by a blank line and prefixed with data:.
const response = await fetch("https://api.example.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "claude-sonnet",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Write a haiku about deploys" }],
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let fullText = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop(); // keep incomplete chunk for next read
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
fullText += event.delta.text;
renderToUI(fullText);
}
}
}
The key detail people get wrong: buffer incomplete chunks. A read() call can return a partial event, so you must hold back the last, possibly-incomplete line and prepend it to the next chunk instead of discarding it.
Handling Tool Use in a Stream
When Claude decides to call a tool mid-response, the stream emits a content_block_start with type: "tool_use", followed by input_json_delta events that stream the tool arguments as partial JSON fragments. You need to concatenate these fragments and only JSON.parse the result once content_block_stop fires — parsing partial JSON on every delta will throw.
let toolInputBuffer = "";
if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
toolInputBuffer = "";
}
if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
toolInputBuffer += event.delta.partial_json;
}
if (event.type === "content_block_stop") {
const toolInput = JSON.parse(toolInputBuffer);
// execute the tool with toolInput
}
This pattern generalizes across tool calls: track buffers per content block index, since a single response can include multiple blocks.
Error Handling and Reconnection
Streams can drop mid-response due to network issues, proxy timeouts, or server-side errors. A few practical rules:
- Set a reasonable read timeout and treat silence past that threshold as a failure, not an infinite wait.
- If a stream errors after partial content was rendered, decide whether to discard the partial output or keep it and append an error notice — don't silently truncate.
- For idempotent requests, a clean retry from scratch is usually simpler than trying to resume a partial stream, since Claude doesn't support resuming from a mid-stream cursor.
- Watch for
errorevents in the SSE stream itself, not just HTTP-level failures — the connection can open successfully at 200 OK and still emit an error event later.
Streaming Through SubToAPI
If you're already routing requests through SubToAPI to turn your Claude access into an application-ready API, streaming works the same way — set "stream": true against https://api.subtoapi.app/v1/messages with your sub_live_... key, and parse SSE exactly as shown above. The streaming docs cover the event schema in detail, and the messages reference documents the full request/response shape including tool use. It's a drop-in target if you want per-application keys and usage metadata without changing your stream-parsing code.
Questions
Does streaming cost more than a non-streaming request? No. Token usage and pricing are based on input and output tokens, not on whether the response is streamed. Streaming only changes delivery, not billing.
Can I cancel a stream partway through? Yes, closing the underlying HTTP connection (aborting the fetch or closing the reader) stops further token generation server-side in most implementations, which also stops you being charged for tokens not yet generated.
Why does my JSON.parse fail on tool_use input? You're likely parsing after every input_json_delta instead of accumulating fragments and parsing once after content_block_stop. Partial JSON strings aren't valid JSON until the block is complete.