← Blog

Claude Streaming Response: Implementation Guide

2026-09-22 · 4 min read · SubToAPI Team

Streaming lets your app render Claude's answer token-by-token instead of waiting for the full response, which matters for chat UIs, coding assistants, and anything where perceived latency affects usability. Implementing it correctly means opening a persistent HTTP connection, parsing Server-Sent Events (SSE) as they arrive, and reassembling incremental deltas into a coherent message on the client.

This guide walks through the actual mechanics: the event types you'll see, how to parse them without dropping data mid-stream, how to handle tool use inside a stream, and how to deal with reconnects and errors. The examples use plain HTTP against a Claude-compatible API so you can apply the same logic whether you're calling Anthropic directly or a proxy like SubToAPI.

How Claude Streaming Works

Streaming responses use the text/event-stream content type over a single long-lived HTTP connection. Instead of one JSON blob, the server sends a sequence of named events, each carrying a small JSON payload. Your client reads the stream incrementally and appends text as it arrives.

A typical stream looks like this, in order:

Knowing this order matters because your parser needs to accumulate deltas per block, not just concatenate raw text blindly — especially once tool use enters the picture.

Making a Streaming Request

To enable streaming, set "stream": true in your request body and read the response as a stream rather than a single JSON payload.

curl -N https://api.example.com/v1/messages \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_KEY" \
  -d '{
    "model": "claude-sonnet",
    "max_tokens": 1024,
    "stream": true,
    "messages": [{"role": "user", "content": "Explain event loops in 3 sentences"}]
  }'

The -N flag disables curl's output buffering so you see events as they come in, rather than all at once at the end.

Parsing SSE on the Client

In the browser or Node, fetch with a ReadableStream reader is the standard approach. Each SSE event is separated by a blank line and prefixed with data:.

const response = await fetch("https://api.example.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${apiKey}`,
  },
  body: JSON.stringify({
    model: "claude-sonnet",
    max_tokens: 1024,
    stream: true,
    messages: [{ role: "user", content: "Write a haiku about deploys" }],
  }),
});

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

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const event = JSON.parse(line.slice(6));

    if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
      fullText += event.delta.text;
      renderToUI(fullText);
    }
  }
}

The key detail people get wrong: buffer incomplete chunks. A read() call can return a partial event, so you must hold back the last, possibly-incomplete line and prepend it to the next chunk instead of discarding it.

Handling Tool Use in a Stream

When Claude decides to call a tool mid-response, the stream emits a content_block_start with type: "tool_use", followed by input_json_delta events that stream the tool arguments as partial JSON fragments. You need to concatenate these fragments and only JSON.parse the result once content_block_stop fires — parsing partial JSON on every delta will throw.

let toolInputBuffer = "";

if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
  toolInputBuffer = "";
}
if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
  toolInputBuffer += event.delta.partial_json;
}
if (event.type === "content_block_stop") {
  const toolInput = JSON.parse(toolInputBuffer);
  // execute the tool with toolInput
}

This pattern generalizes across tool calls: track buffers per content block index, since a single response can include multiple blocks.

Error Handling and Reconnection

Streams can drop mid-response due to network issues, proxy timeouts, or server-side errors. A few practical rules:

Streaming Through SubToAPI

If you're already routing requests through SubToAPI to turn your Claude access into an application-ready API, streaming works the same way — set "stream": true against https://api.subtoapi.app/v1/messages with your sub_live_... key, and parse SSE exactly as shown above. The streaming docs cover the event schema in detail, and the messages reference documents the full request/response shape including tool use. It's a drop-in target if you want per-application keys and usage metadata without changing your stream-parsing code.

Questions

Does streaming cost more than a non-streaming request? No. Token usage and pricing are based on input and output tokens, not on whether the response is streamed. Streaming only changes delivery, not billing.

Can I cancel a stream partway through? Yes, closing the underlying HTTP connection (aborting the fetch or closing the reader) stops further token generation server-side in most implementations, which also stops you being charged for tokens not yet generated.

Why does my JSON.parse fail on tool_use input? You're likely parsing after every input_json_delta instead of accumulating fragments and parsing once after content_block_stop. Partial JSON strings aren't valid JSON until the block is complete.

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 →