Claude API Streaming: Token-by-Token Output Guide
If you want Claude to return text progressively — word by word, the way ChatGPT and Claude.ai render responses — you need to enable streaming on your API request instead of waiting for the full completion. Streaming sends the response as a sequence of server-sent events (SSE), where each event contains a small chunk of generated text (a token or a few tokens), so your UI can display output as it's produced instead of showing a blank screen until generation finishes.
This matters for two reasons: perceived latency and UX. A 500-token response might take 4-8 seconds to generate in full. Without streaming, the user stares at a spinner for the entire duration. With streaming, the first tokens appear within a few hundred milliseconds and the rest trickles in, which feels dramatically faster even though total generation time is identical. Below is exactly how token-by-token streaming works, how to implement it, and the pitfalls that trip people up.
How streaming actually works
When you set "stream": true in your request, the API keeps the HTTP connection open and sends a series of SSE events instead of one JSON blob. The event stream typically includes:
message_start— metadata about the response (model, id, role)content_block_start— a content block begins (text or tool use)content_block_delta— the actual incremental text, sent repeatedly as generation proceedscontent_block_stop— the block is completemessage_delta— final metadata like stop reasonmessage_stop— the stream is finished
The piece you care about for "token by token" output is content_block_delta. Each event in this type carries a small fragment of text under delta.text. Concatenating all of these in order reconstructs the full response.
Streaming with curl
A raw streaming request against an Anthropic-compatible API looks like this:
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": 512,
"stream": true,
"messages": [
{"role": "user", "content": "Write a haiku about distributed systems."}
]
}'
With stream: true, the terminal prints raw SSE lines as they arrive — you'll see event: content_block_delta lines interleaved with data: {...} payloads, appearing progressively instead of all at once.
Streaming with JavaScript
In a browser or Node environment, you consume the stream by reading the response body incrementally rather than calling .json(). Here's a minimal fetch-based implementation:
async function streamCompletion(prompt) {
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-5",
max_tokens: 512,
stream: true,
messages: [{ role: "user", content: prompt }],
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n").filter((l) => l.startsWith("data:"));
for (const line of lines) {
const json = line.replace("data: ", "").trim();
if (json === "[DONE]" || !json) continue;
try {
const event = JSON.parse(json);
if (event.type === "content_block_delta") {
const text = event.delta?.text || "";
fullText += text;
process.stdout.write(text); // render token by token
}
} catch {
// partial chunk, ignore and wait for more data
}
}
}
return fullText;
}
The key detail: SSE chunks don't always align with event boundaries. A single read() call might return half an event, or multiple events at once. Always buffer incomplete lines and parse defensively — wrapping JSON.parse in a try/catch (or maintaining a buffer until you see a full data: line) prevents crashes on partial payloads.
Rendering tokens in a UI
For a chat interface, the pattern is usually:
- Append a new empty message bubble when
message_startfires. - On each
content_block_delta, appenddelta.textto that bubble's content state. - On
message_stop, mark the message as complete (stop showing a typing cursor, re-enable the input box).
If you're using React, this means updating state on every delta event, which can be dozens of times per second for a fast response. Debouncing re-renders (batching deltas every 30-50ms) keeps the UI smooth without dropping any text — you still display everything, just not on every single token.
Common streaming pitfalls
- Forgetting to handle tool-use blocks. If Claude calls a tool mid-stream, you'll get
content_block_startwithtype: "tool_use"instead of text, and the delta events carry partial JSON (input_json_delta) rather than plain text. See /docs/tools for the full event shapes. - Timeouts on long responses. Some HTTP clients and proxies have idle timeouts that kill a connection if there's a gap between chunks. Keep-alive settings and reasonable
max_tokensvalues avoid this. - Not handling
stop_reason. Streams can end early due tomax_tokens, a stop sequence, or tool use — check the finalmessage_deltaevent to know why generation stopped. - Billing confusion. Streaming doesn't change token counts or cost — it only changes delivery format. You're billed the same either way.
SubToAPI exposes the same SSE streaming interface over your existing Claude access, so any client built against the Anthropic streaming format works without modification — see /docs/streaming for the full event reference and /docs/quickstart to get an API key in under five minutes.
Questions
Does streaming cost more than a non-streaming request? No. Token usage and pricing are identical — streaming only changes how the response is delivered (incrementally vs. all at once), not how many tokens are generated or billed.
Can I stream and also use tool calling at the same time? Yes. Streamed tool-use responses send input_json_delta events that build up the tool call's arguments incrementally, in addition to content_block_delta for any plain text output.
What happens if the connection drops mid-stream? You lose whatever hasn't arrived yet and need to retry the request from scratch — streaming doesn't support resuming a partial response, so your client should treat a dropped connection as a failed request.