← Blog

Claude API Streaming: Server-Sent Events Tutorial

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

Streaming lets you display Claude's response token by token instead of waiting for the full completion. The Claude API implements this using server-sent events (SSE), a simple HTTP-based protocol where the server keeps a connection open and pushes small chunks of data as they're generated. This tutorial shows exactly how that stream is structured and how to consume it correctly in curl, raw JavaScript, and through the fetch API.

If you've ever gotten partial JSON errors, cut-off text, or garbled output when trying to parse Claude's streaming responses, it's almost always because the SSE event structure wasn't handled correctly. We'll fix that here with working code.

How SSE Works in the Claude API

When you set "stream": true in your request, the API responds with Content-Type: text/event-stream instead of a single JSON blob. The body is a sequence of events, each one looking like this:

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

Each event has a event: line naming the event type, and a data: line containing JSON. Events are separated by a blank line. The key event types you'll encounter, in order:

You need to accumulate content_block_delta events to reconstruct the full text. There's no single "final" event containing the whole message — you build it yourself from the deltas.

Raw curl Example

This is useful for debugging what the wire format actually looks like:

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": 512,
    "stream": true,
    "messages": [{"role": "user", "content": "Write a haiku about rain"}]
  }'

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

Parsing SSE in JavaScript

Node's fetch gives you a readable stream, but you have to parse the SSE framing yourself — split on double newlines, strip the data: prefix, and handle partial chunks that arrive mid-event:

async function streamMessage() {
  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: 512,
      stream: true,
      messages: [{ role: "user", content: "Write a haiku about rain" }],
    }),
  });

  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 event for next loop

    for (const event of events) {
      const line = event.split("\n").find((l) => l.startsWith("data:"));
      if (!line) continue;
      const json = JSON.parse(line.slice(5).trim());
      if (json.type === "content_block_delta" && json.delta?.text) {
        fullText += json.delta.text;
        process.stdout.write(json.delta.text);
      }
    }
  }
  return fullText;
}

The important detail is keeping the last, possibly incomplete, chunk in buffer for the next read instead of trying to parse it immediately — network chunks don't align neatly with SSE event boundaries.

Common Mistakes

Trying to JSON.parse the whole response body. It's not one JSON object — it's a sequence of SSE frames. Parse each data: line individually.

Ignoring message_delta events. This is where stop_reason and final usage numbers show up. If you only listen for text deltas, you'll miss why the stream ended (e.g., max_tokens vs end_turn).

Not handling tool use streams. If Claude is calling a tool, you'll get content_block_start with type: tool_use, followed by input_json_delta events that stream the arguments as partial JSON strings. You need to concatenate those fragments and parse the full JSON only once content_block_stop fires.

Forgetting to handle connection drops. Long streams over flaky networks can disconnect mid-response. Production code should detect an unexpected close (no message_stop received) and either retry or surface a clear error to the user rather than silently truncating.

Streaming Through SubToAPI

If you're already calling Claude through SubToAPI, the same SSE contract applies — you point your client at https://api.subtoapi.app/v1/messages with your sub_live_... key instead of managing raw Anthropic credentials, and the streaming format, event types, and parsing logic above work unchanged. SubToAPI adds per-request usage metadata you can read from the stream, which is useful for billing dashboards or rate-limiting logic. See the streaming docs for the full event reference, or the quickstart if you're setting up a key for the first time. Full request/response shapes are in the messages docs.

Whether you're calling Claude directly or through a proxy layer, the underlying SSE mechanics don't change — get the parsing loop right once and it works everywhere.

questions

Does Claude API streaming use WebSockets? No. It uses server-sent events (SSE) over a standard HTTP connection with Content-Type: text/event-stream. You send one POST request with stream: true and read the response body as a continuous stream rather than opening a separate socket.

Why is my streamed text missing characters or garbled? This usually happens when you parse network chunks directly instead of buffering until you have a complete SSE event (a full block ending in a blank line). Always accumulate partial data and only parse data: lines once you've found the double-newline delimiter.

Can I stream tool use (function calling) responses? Yes. Tool arguments arrive as input_json_delta fragments inside content_block_delta events. Concatenate the fragments into a single string and parse it as JSON only after you receive content_block_stop for that block — partial fragments are not valid JSON on their own.

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 →