← Blog

How to Stream Claude Responses in Node.js

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

Streaming Claude responses in Node means opening a request that stays connected while the model generates tokens, then reading and forwarding those tokens as they arrive instead of waiting for the full completion. The mechanism is Server-Sent Events (SSE): the server sends a sequence of event: / data: lines over a single HTTP response, and your Node process reads that response body as a stream, parsing each chunk as it lands.

This matters for anything with a chat UI, a CLI tool, or a voice interface — users perceive a token-by-token response as instant, even if total generation time is identical to a blocking call. Below is a working implementation using Node's built-in fetch, followed by the event types you need to handle correctly, and how the same code looks if you're calling Claude through an API layer like SubToAPI instead of managing raw provider streams yourself.

Prerequisites

You don't need a special SSE library. The response body from fetch is a ReadableStream, and you can decode it with TextDecoder and split on newlines manually. This is the same technique regardless of which provider or proxy sits behind the API.

Basic streaming request

Here's a minimal streaming call against a Messages-style API:

async function streamCompletion(prompt) {
  const response = await fetch("https://your-api-host/v1/messages", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${process.env.API_KEY}`,
    },
    body: JSON.stringify({
      model: "claude-sonnet-4",
      max_tokens: 1024,
      stream: true,
      messages: [{ role: "user", content: prompt }],
    }),
  });

  if (!response.ok || !response.body) {
    throw new Error(`Request failed: ${response.status}`);
  }

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

    for (const line of lines) {
      if (!line.startsWith("data:")) continue;
      const payload = line.slice(5).trim();
      if (payload === "[DONE]") return;

      const event = JSON.parse(payload);
      if (event.type === "content_block_delta") {
        process.stdout.write(event.delta.text);
      }
    }
  }
}

The critical detail most implementations get wrong is buffering: a single read() call from the network can return a partial line, or several events joined together. You must accumulate into a buffer, split on newlines, and hold back the last (possibly incomplete) fragment for the next chunk. Skipping this causes intermittent JSON.parse failures under real network conditions — it often works fine locally and breaks in production.

Handling stream event types

A well-formed SSE stream from a Claude-compatible API sends distinct event types, not just raw text deltas:

If you're building anything beyond a simple text echo — tool use, multi-block responses, token accounting — branch on event.type explicitly rather than assuming every event is a text delta:

switch (event.type) {
  case "content_block_delta":
    if (event.delta.type === "text_delta") {
      process.stdout.write(event.delta.text);
    }
    break;
  case "message_delta":
    console.log("\nUsage:", event.usage);
    break;
  case "message_stop":
    console.log("\nStream complete.");
    break;
}

Piping the stream to an HTTP response

If you're relaying this to a browser client, pipe the same pattern through your own SSE endpoint instead of buffering the full response server-side:

app.post("/chat", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  const upstream = await fetch("https://your-api-host/v1/messages", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.API_KEY}`,
    },
    body: JSON.stringify({ ...req.body, stream: true }),
  });

  upstream.body.pipe(res);
});

Piping the raw upstream body directly avoids re-serializing every chunk and keeps latency close to the theoretical minimum.

Streaming with SubToAPI

If you're already routing Claude calls through SubToAPI, streaming works the same way — the endpoint accepts stream: true and returns standard SSE, so the code above needs zero changes beyond the host and key:

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4",
    "max_tokens": 1024,
    "stream": true,
    "messages": [{"role": "user", "content": "Write a haiku about Node.js streams"}]
  }'

The advantage is that you get application-scoped keys (sub_live_...), per-key usage metadata, and team seat management on top of the same streaming behavior, instead of building key rotation and usage logging yourself. Full event reference and edge cases are in /docs/streaming, and the general request/response shape is in /docs/messages. If you're setting this up for the first time, /docs/quickstart walks through generating a key and making your first call.

questions

Do I need a special SSE library for Node? No. Node 18+ has native fetch with a readable stream body, which is enough to parse SSE manually. Libraries like eventsource-parser help if you want stricter spec compliance, but a manual buffer-and-split loop is sufficient for most apps.

Why does my stream occasionally throw a JSON parse error? Almost always a buffering bug — a network chunk cut a line in the middle. Accumulate incoming text into a buffer, split on newlines, and only parse complete lines, keeping the trailing partial line for the next read.

Can I stream tool use output the same way? Yes, but tool inputs arrive as content_block_delta events with input_json_delta fragments that you must concatenate and parse once the block closes, rather than treating them as plain text. See /docs/tools for the exact event sequence.

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 →