← Blog

Claude API Streaming: Chunked Response Example

2026-10-17 · 4 min read · SubToAPI Team

When you set "stream": true on a Claude API request, the response doesn't come back as one JSON blob. It comes back as a chunked HTTP response using server-sent events (SSE), where the model's output arrives as a sequence of small, typed events over an open connection. Each chunk represents a fragment of the final message — a content delta, a tool call update, or a lifecycle marker like "message started" or "message stopped."

This article shows exactly what those chunks look like on the wire, how to parse them correctly, and a working example you can copy into a project. If you've only ever consumed streaming through a high-level SDK, seeing the raw chunked format makes debugging a lot easier.

What a chunked streaming response actually contains

A Claude streaming response is a series of event: <type> / data: <json> pairs, separated by blank lines. A typical sequence looks like this:

event: message_start
data: {"type":"message_start","message":{"id":"msg_01...","model":"claude-...","role":"assistant","content":[],"usage":{"input_tokens":25,"output_tokens":0}}}

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}

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"}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":12}}

event: message_stop
data: {"type":"message_stop"}

Each content_block_delta event carries one small piece of text. The client is responsible for concatenating delta.text fields in order to reconstruct the full message. There's no single "complete response" chunk — the final text is the sum of all deltas up to message_stop.

The event types you'll actually handle

If you're also using tool calling, tool inputs stream in as input_json_delta chunks inside content_block_delta, and you need to buffer and parse the JSON only once the block is complete — partial JSON won't parse.

Parsing chunked responses with curl

curl will print the raw chunked stream as it arrives, which is the fastest way to inspect the format without writing code:

curl -N https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 256,
    "stream": true,
    "messages": [{"role": "user", "content": "Count to 5"}]
  }'

The -N flag disables curl's output buffering so you see events as they land instead of all at once at the end.

Parsing chunked responses in JavaScript

In Node or the browser, you read the response body as a stream and split on double newlines to separate SSE events:

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 256,
    stream: true,
    messages: [{ role: "user", content: "Count to 5" }],
  }),
});

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 events = buffer.split("\n\n");
  buffer = events.pop(); // keep incomplete chunk for next read

  for (const chunk of events) {
    const line = chunk.split("\n").find((l) => l.startsWith("data: "));
    if (!line) continue;
    const payload = JSON.parse(line.slice(6));

    if (payload.type === "content_block_delta" && payload.delta.text) {
      fullText += payload.delta.text;
      process.stdout.write(payload.delta.text);
    }
  }
}

The key detail: chunk boundaries from the transport layer don't line up with SSE event boundaries. A single read() call might return half an event, or three events glued together. You always need a buffer that accumulates bytes and only processes complete event\n\ndata\n\n blocks, pushing the leftover back for the next read.

Why streaming output through an API layer is harder than it looks

If you're exposing Claude's streaming behavior to your own app or to external clients, you now own all of the above parsing logic, plus reconnect handling, backpressure, and making sure partial tool-call JSON doesn't leak to consumers before it's valid. This is exactly the kind of plumbing SubToAPI handles for you — it wraps the same chunked SSE format behind a stable sub_live_... key, so your client code just consumes a clean stream without reimplementing buffer logic for every language you support. See the streaming docs for the exact event shapes, or the quickstart to get a key in a couple minutes. Plans start at €9/month with a free trial — details on pricing.

Questions

Why does my parser get malformed JSON mid-stream? You're probably parsing on every read() call instead of buffering. Chunk boundaries from the network don't align with SSE event boundaries — accumulate text and only parse complete data: lines.

Can I get a single final JSON response instead of chunks? Yes — just set "stream": false and call /v1/messages normally. Streaming is opt-in and meant for incremental UI rendering, not required for every request.

How do I handle tool_use content in a streaming response? Tool inputs arrive as input_json_delta fragments inside content_block_delta events. Buffer them per content block index and only JSON.parse once you see the matching content_block_stop.

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 →