← Blog

Claude API Streaming: Parsing Chunked Responses Right

2026-10-25 · 5 min read · SubToAPI Team

When you stream a Claude API response, you don't get one clean JSON blob back — you get a sequence of small chunks sent over an open HTTP connection, each one representing a partial update to the model's output. Parsing these correctly means handling Server-Sent Events (SSE) framing, reassembling text deltas in order, and dealing with event types you might not expect (tool calls, stop reasons, usage metadata). Get the parsing wrong and you'll see garbled text, dropped tokens, or a client that hangs waiting for a chunk that never arrives.

This article walks through exactly how chunked streaming responses are structured, how to parse them safely in Node and the browser, and the specific edge cases that trip people up: partial UTF-8 characters split across chunks, multi-event buffers, and reconnect handling.

How Claude's streaming response is framed

Claude's streaming API uses SSE over a standard HTTP connection with Content-Type: text/event-stream. Each event looks like this on the wire:

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}

Key things to understand about this framing:

This is the root cause of most "streaming parsing bugs": developers assume one data: write from the server equals one chunk at the TCP level. It doesn't. HTTP chunked transfer encoding and SSE framing are two separate layers — your code needs to handle both.

Parsing chunks correctly in Node.js

The safe pattern is: accumulate raw bytes in a buffer, split on double newlines, process complete events, and keep the remainder for the next read.

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-5",
    stream: true,
    max_tokens: 1024,
    messages: [{ role: "user", content: "Write a haiku about parsing." }]
  })
});

const reader = res.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 events = buffer.split("\n\n");
  buffer = events.pop(); // keep the incomplete tail

  for (const raw of events) {
    const line = raw.split("\n").find(l => l.startsWith("data:"));
    if (!line) continue;

    const json = JSON.parse(line.replace("data:", "").trim());

    if (json.type === "content_block_delta") {
      process.stdout.write(json.delta.text);
    }
    if (json.type === "message_stop") {
      console.log("\n--- stream complete ---");
    }
  }
}

Two details matter here that are easy to miss:

decoder.decode(value, { stream: true }) — passing stream: true tells TextDecoder to hold back any incomplete multi-byte UTF-8 sequence instead of corrupting it. Without this, emoji and non-Latin text get mangled when a character is split across two chunks.

Keeping the tail with buffer.split("\n\n").pop() — this is what prevents you from processing a half-written JSON object. If you parse every line as soon as it arrives without this buffering step, you'll get intermittent JSON.parse errors on larger responses, and they'll be nearly impossible to reproduce locally because they depend on network chunk boundaries.

Handling the different event types

A full Claude stream includes more than just text deltas. A parser that only looks for content_block_delta will work until the model calls a tool or you need usage numbers for billing. The event types you should branch on:

If your application uses tool calling, tool input arrives as a stream of partial JSON strings inside content_block_delta events with type: "input_json_delta" — you accumulate the string fragments and JSON.parse the whole thing only once the block stops. Parsing it mid-stream will throw repeatedly, which is expected behavior, not a bug.

Reconnects and dropped connections

Chunked streams can drop mid-response — a proxy timeout, a flaky mobile connection, a server restart. Your parser needs a strategy for this independent of the JSON parsing logic:

If you're building this yourself across multiple provider integrations, you end up re-solving the same buffering, UTF-8, and reconnect logic for each one. SubToAPI exposes Claude through a single streaming endpoint with the same SSE format described above, so the parsing code you write once works whether you're calling it from a Node service, a browser client, or a mobile backend. See the streaming docs for the full event reference and a working example, or start with the quickstart if you haven't set up an API key yet.

FAQ

Why does my JSON.parse fail intermittently on streamed chunks? You're almost certainly parsing raw chunk data directly instead of buffering until a full SSE event (ending in a blank line) has arrived. Fix it by accumulating bytes in a buffer and only parsing complete events.

Why do I see corrupted characters in streamed text? Multi-byte UTF-8 characters can be split across two network chunks. Use TextDecoder with { stream: true } so it holds incomplete byte sequences until the rest arrives, instead of decoding partial bytes immediately.

How do I get token usage data from a streaming response? Usage information arrives incrementally in message_delta events and is finalized in the last event before message_stop. Don't rely on a single final response object the way you would with a non-streaming call — accumulate usage fields as they update.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →