Claude API Streaming: A Server-Sent Events Example
If you're looking for a working example of Claude API streaming with Server-Sent Events (SSE), here's the short version: you send a request with "stream": true, the server keeps the HTTP connection open, and it pushes a sequence of event: / data: lines as the model generates tokens. Your client reads these events incrementally instead of waiting for the full response, which is what makes chat UIs feel responsive instead of frozen for 10 seconds.
This matters because without streaming, a long completion blocks until the entire response is generated — bad for UX, and risky for request timeouts on serverless platforms. With SSE, you start rendering text the moment the first token arrives, and you can show typing indicators, cancel generation mid-stream, or pipe tokens straight into a frontend component.
How Claude's SSE Stream Is Structured
When you enable streaming, the response isn't JSON — it's a stream of text events, each prefixed with event: and followed by a data: line containing a JSON payload. The event types you'll typically see, in order, are:
message_start— the response begins, includes the message ID and modelcontent_block_start— a content block (text, tool call, etc.) beginscontent_block_delta— the actual token-by-token text chunkscontent_block_stop— that content block is finishedmessage_delta— usage/stop reason updatesmessage_stop— the stream is complete
Each content_block_delta event carries a small JSON object like {"type":"text_delta","text":"Hello"}. You concatenate these fragments in order to reconstruct the full message as it's generated.
Raw curl Example
Here's what a streaming request looks like directly against an SSE-compatible Claude endpoint using curl with the -N flag (disables buffering so you see events as they arrive):
curl -N https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4",
"max_tokens": 512,
"stream": true,
"messages": [
{"role": "user", "content": "Write a haiku about debugging."}
]
}'
You'll see output scroll in your terminal line by line as events arrive, instead of a single blob printed after a pause.
Parsing SSE in JavaScript
In the browser or Node, you consume the response body as a stream and split it on newlines to extract each data: payload. Here's a minimal parser that works against any SSE-compliant Claude-compatible endpoint:
async function streamMessage(prompt) {
const res = 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: 512,
stream: true,
messages: [{ role: "user", content: prompt }],
}),
});
const reader = res.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");
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") {
const chunk = event.delta?.text ?? "";
fullText += chunk;
process.stdout.write(chunk); // or append to DOM
}
}
}
return fullText;
}
The key detail most people miss on their first attempt: network chunks don't align with SSE event boundaries. You have to buffer partial lines and only process complete data: lines — otherwise JSON.parse throws on truncated payloads.
Handling Tool Use and Stop Reasons in a Stream
If your request includes tool definitions, streaming adds content_block_start events with type: "tool_use" and incremental input_json_delta chunks that you concatenate into a full JSON string before parsing. Check message_delta.delta.stop_reason to know whether the model stopped naturally, hit max_tokens, or paused to call a tool. Details on request structure are in /docs/messages, and tool-specific streaming behavior is covered in /docs/tools.
Why Use a Gateway Instead of Raw Streaming Code
Writing your own SSE parser is a one-time cost, but maintaining it across reconnect logic, partial-chunk edge cases, and usage tracking adds up. SubToAPI exposes the same streaming interface over a stable sub_live_ key, so your frontend and backend code doesn't need separate credentials, and usage metadata (tokens in/out, cost) is logged automatically per key — useful if multiple apps or team members share the same underlying Claude access. Setup takes a few minutes; see /docs/quickstart for an end-to-end example, or /docs/streaming for streaming-specific options like event filtering.
Plans start at €9/month for solo use, with team seats at €19 and higher-volume Scale seats at €49 — all with a free trial at /signup. Full details are on /pricing.
Common Streaming Mistakes to Avoid
- Buffering the whole response client-side before rendering — defeats the purpose of streaming; render deltas as they arrive.
- Not handling
pingor keep-alive events — some infrastructure sends periodic pings to keep the connection alive; your parser should ignore unknown event types gracefully rather than crashing. - Forgetting to set
Accept: text/event-streamon strict proxies or CDNs that otherwise buffer the response and break real-time delivery. - Blocking on
JSON.parsefor partial lines — always wait for a full line before parsing.
questions
Does Claude API streaming use standard SSE format? Yes. Responses use the event:/data: convention with named event types like content_block_delta, so any standard SSE client or the fetch + ReadableStream approach shown above works.
Can I cancel a stream mid-generation? Yes — close the underlying connection (abort the fetch request or end the HTTP connection) and the server stops generating further tokens, saving on output token costs.
Does streaming cost more than non-streaming requests? No, token usage and pricing are identical. Streaming only changes delivery timing, not the number of tokens billed.