Claude API Streaming Chunk Parsing Example
When you stream a response from the Claude API, you don't get one JSON blob back — you get a sequence of server-sent events (SSE), each carrying a small piece of the final message. Parsing those chunks correctly means handling several distinct event types, buffering partial data, and reassembling text and tool calls in the right order. This article walks through a full working example in JavaScript, explains each event type you'll encounter, and covers the edge cases that trip people up (partial JSON, multi-block messages, reconnects).
If you just want the short answer: read the response body as a stream, split it on double newlines to get individual SSE events, parse the event: and data: lines, and switch on the event type (message_start, content_block_delta, message_stop, etc.) to build up your output incrementally. The rest of this post shows exactly how.
Why streaming responses need special parsing
A non-streaming Claude API call returns a single JSON object with a content array. Streaming trades that simplicity for lower latency: the server sends the response as it's generated, so your app can render tokens as they arrive instead of waiting for the full completion.
The tradeoff is that your client now has to reconstruct the final message from a series of events. Each event describes a small mutation — "start a new content block," "append this text delta," "stop this block," "here's the final usage." If you mishandle any of these, you end up with garbled text, dropped tokens, or broken JSON when tool use is involved.
The event types you'll see
A typical Claude streaming response emits these events in order:
message_start— the message shell, including id, role, and empty contentcontent_block_start— a new block begins (text or tool_use)content_block_delta— incremental content:text_deltafor text,input_json_deltafor tool argumentscontent_block_stop— the current block is completemessage_delta— top-level changes likestop_reasonand cumulative usagemessage_stop— the stream is finished
Each SSE frame looks like this on the wire:
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
Note the blank line after data: — that's the frame delimiter. Any parser you write needs to split on that, not on individual newlines, because a single data: payload is never itself split across lines by the API.
A minimal parsing example
Here's a self-contained example using fetch and a ReadableStream reader. It works in Node 18+ and in browsers.
async function streamMessage(url, options) {
const response = await fetch(url, options);
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 });
// SSE frames are separated by a blank line
const frames = buffer.split("\n\n");
buffer = frames.pop(); // keep the last (possibly incomplete) frame
for (const frame of frames) {
const lines = frame.split("\n");
let eventType = null;
let data = null;
for (const line of lines) {
if (line.startsWith("event:")) eventType = line.slice(6).trim();
if (line.startsWith("data:")) data = line.slice(5).trim();
}
if (!data) continue;
const payload = JSON.parse(data);
switch (eventType) {
case "content_block_delta":
if (payload.delta?.type === "text_delta") {
fullText += payload.delta.text;
process.stdout.write(payload.delta.text); // render as it arrives
}
break;
case "message_stop":
console.log("\n--- stream complete ---");
break;
}
}
}
return fullText;
}
The key details that make this robust:
- Buffer partial frames. A chunk from
reader.read()doesn't align with SSE frame boundaries. Always keep the last, possibly incomplete, piece in the buffer and only parse complete frames. - Use
{ stream: true }on the decoder. This prevents theTextDecoderfrom mangling multi-byte UTF-8 characters that get split across chunks — important for non-ASCII text. - Switch on
eventType, not on the shape of the payload. Thetypefield inside the JSON body is a decent fallback if your SSE client drops theevent:line, but relying on the explicit event name is more reliable.
Handling tool use in the stream
If your request includes tools, you'll also see input_json_delta events inside content_block_delta, where the tool's input field is streamed as fragments of a JSON string:
case "content_block_delta":
if (payload.delta?.type === "input_json_delta") {
toolInputBuffer += payload.delta.partial_json;
}
break;
You cannot JSON.parse toolInputBuffer until you receive the matching content_block_stop — the fragments are not valid JSON on their own. Accumulate them as a string and parse once the block closes.
Simplifying this with SubToAPI
Writing and maintaining this parser is a one-time cost, but if you're exposing Claude access to a team or a product, you also need application-level API keys, per-key usage tracking, and a stable place to manage that without touching your core provider credentials. SubToAPI sits in front of Claude and gives you sub_live_... keys, streaming, tool use and usage metadata through one HTTPS API, so the chunk-parsing logic above works unchanged against https://api.subtoapi.app/v1/messages — you just point your existing SSE client at a different base URL and key.
The streaming endpoint is documented at /docs/streaming, request/response shapes at /docs/messages, and tool-calling specifics at /docs/tools. If you're setting up your first integration, /docs/quickstart covers key creation end to end, and /signup starts a free trial with no upfront plan commitment — paid plans start at Solo €9 when you're ready (see /pricing).
Common mistakes to avoid
- Splitting on
\ninstead of\n\n. This breaks multi-line data payloads and causes intermittent parse failures that are hard to reproduce. - Parsing
data:before checking for[DONE]or empty payloads. Some SSE implementations send a final sentinel; guard yourJSON.parsecall. - Discarding
message_deltausage data. The final token counts often arrive inmessage_delta, notmessage_stop— if you're logging usage, read from the right event. - Assuming one content block. Claude can stream multiple blocks (e.g., text followed by a tool call) in a single message; track blocks by
index, not by assuming a single running string.
Questions
Do I need a special SSE library to parse Claude API streaming chunks? No. A ReadableStream reader plus a small buffering loop, as shown above, is enough. Libraries like eventsource-parser can save boilerplate but aren't required for basic use cases.
Why does my parsed text sometimes have missing or garbled characters? This usually happens when multi-byte UTF-8 characters are split across chunk boundaries. Always call TextDecoder.decode() with { stream: true } until the final chunk.
How do I know when a tool call's JSON input is complete? Wait for the content_block_stop event matching that block's index before parsing the accumulated partial_json fragments — they aren't valid JSON individually.