← Blog

Claude API Streaming Chunk Parsing in JavaScript

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

When you stream a response from the Claude API, you don't get one clean JSON object back — you get a sequence of server-sent events (SSE), each carrying a fragment of the final message. Parsing those chunks correctly in JavaScript means handling partial lines, multiple event types, and incremental text deltas without dropping or duplicating content.

This article shows exactly how the chunk format works and gives you working JavaScript code to parse it reliably, whether you're hitting the Anthropic API directly or a compatible endpoint like SubToAPI's.

What a Claude streaming response actually looks like

When you set "stream": true on a messages request, the server responds with Content-Type: text/event-stream. Instead of one payload, you receive a series of events separated by blank lines, each prefixed with event: and data::

event: message_start
data: {"type":"message_start","message":{"id":"msg_01...","role":"assistant","content":[]}}

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

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

The important part for parsing: the actual text you want to display lives in content_block_delta events, specifically delta.text. Everything else is bookkeeping — lifecycle markers and usage metadata.

Why naive parsing breaks

A common mistake is reading the response body as plain text and splitting on newlines, assuming each chunk from the network maps to one SSE event. It doesn't. TCP and HTTP chunked transfer can split a single SSE event across two reads, or bundle multiple events into one read. If you parse without buffering, you'll get malformed JSON or truncated text at random intervals — usually invisible in testing and painfully visible in production under load.

The fix is to buffer incoming bytes, split on the double-newline that separates SSE events, and only parse complete event blocks, carrying any incomplete tail over to the next chunk.

A correct SSE parser in JavaScript

Here's a fetch-based reader that handles buffering correctly:

async function streamClaudeMessage(url, headers, body, onText) {
  const response = await fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify({ ...body, stream: true }),
  });

  const reader = response.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 });

    // SSE events are separated by a blank line
    const parts = buffer.split("\n\n");
    buffer = parts.pop(); // keep the incomplete tail for next iteration

    for (const part of parts) {
      const lines = part.split("\n");
      let eventType = "message";
      let data = "";

      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;

      let parsed;
      try {
        parsed = JSON.parse(data);
      } catch {
        continue; // skip malformed/partial data, shouldn't happen with proper buffering
      }

      if (parsed.type === "content_block_delta" && parsed.delta?.type === "text_delta") {
        onText(parsed.delta.text);
      }

      if (parsed.type === "message_stop") {
        return;
      }
    }
  }
}

Usage is straightforward:

let fullText = "";

await streamClaudeMessage(
  "https://api.anthropic.com/v1/messages",
  {
    "x-api-key": process.env.ANTHROPIC_API_KEY,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  {
    model: "claude-opus-4-20250514",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Write a haiku about parsing" }],
  },
  (chunk) => {
    fullText += chunk;
    process.stdout.write(chunk);
  }
);

The key design choices here: buffer raw bytes until you have a full event block, discard the incomplete tail rather than trying to parse it, and only act on content_block_delta events for display text. You still need to handle message_delta if you care about final usage numbers, and content_block_start/stop if you're supporting multiple content blocks (e.g. text alongside tool calls).

Handling tool use blocks in the stream

If the model calls a tool mid-response, the stream includes content_block_start with content_block.type === "tool_use", followed by input_json_delta events that stream the arguments as partial JSON strings. You accumulate those deltas per block index and JSON.parse the full string only after content_block_stop. Trying to parse partial JSON mid-stream will throw — don't do it. See the SubToAPI tool use docs for a worked example of accumulating tool input deltas.

Using an SSE client instead of raw fetch

If you don't want to hand-roll buffering logic, libraries like eventsource-parser or the EventSource API (browser-only, GET requests only) handle the blank-line splitting for you. For POST-based streaming in Node or server environments, a manual buffer like the one above is usually simpler than pulling in a dependency.

Simplifying this with SubToAPI

If you're building this against your own Claude access rather than a direct Anthropic API key, SubToAPI turns your Claude subscription into a standard HTTPS API with sub_live_... keys, so the same SSE parsing logic applies — you just point the URL at https://api.subtoapi.app/v1/messages and set Authorization: Bearer $SUBTOAPI_KEY. The event format matches what's shown above, so existing parsers don't need changes. Check the streaming docs for endpoint details, or the quickstart to get a key from the pricing page.

Questions

Why does my JSON.parse fail intermittently on stream chunks? You're parsing data before a full SSE event has arrived. Buffer bytes until you see the blank-line delimiter, then parse only complete blocks — never parse an event that hasn't fully arrived.

Which event type contains the actual text content? content_block_delta events with delta.type === "text_delta" carry the text fragments. Other events (message_start, content_block_start/stop, message_stop) are structural markers, not content.

Can I use the browser EventSource API for this? Only for GET requests without custom headers. Since Claude API streaming requires POST with an Authorization header, you need fetch with a ReadableStream reader, not EventSource, in both browser and Node environments.

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 →