Claude API Streaming Response in Node.js: Full Example
If you're building a chat UI, a CLI tool, or any product where users wait for Claude's output, you want tokens appearing as they're generated instead of staring at a spinner until the full response lands. That's what streaming does, and in Node.js you have two practical ways to get it: the official Anthropic SDK with its built-in stream helper, or a raw fetch call that parses Server-Sent Events (SSE) yourself.
This article walks through both approaches with working code, explains the event types you'll see in the stream, and covers the error-handling and backpressure issues that trip people up in production.
Why stream at all
Without streaming, your Node process sends a request and blocks until Claude finishes generating the entire response — which for longer outputs can be several seconds or more. Streaming returns partial content as it's generated, so you can:
- Render text to a chat UI token-by-token
- Start processing partial output before the full response completes
- Give users a responsive feel even when total generation time is unchanged
- Detect early that a response is going off-track and abort the request
The tradeoff is complexity: you need to handle a chunked, event-based response instead of a single JSON payload.
Option 1: using the official Anthropic SDK
The @anthropic-ai/sdk package exposes a .stream() helper that abstracts away the SSE parsing for you.
npm install @anthropic-ai/sdk
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function streamResponse() {
const stream = anthropic.messages.stream({
model: "claude-opus-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain event loops in Node.js" }],
});
stream.on("text", (textDelta) => {
process.stdout.write(textDelta);
});
const finalMessage = await stream.finalMessage();
console.log("\n--- done ---");
console.log(finalMessage.usage);
}
streamResponse();
The .on("text", ...) listener fires for every incremental chunk of generated text. stream.finalMessage() resolves once the stream closes, giving you the assembled message object including token usage — useful if you're tracking cost per request.
This is the recommended approach for most Node.js apps: fewer moving parts, built-in reconnection handling, and typed events.
Option 2: raw fetch and manual SSE parsing
Sometimes you don't want the SDK dependency, or you're calling an API endpoint that speaks SSE directly (which is also how you'd stream from a proxy like SubToAPI). Here's the manual version using Node's native fetch:
async function streamWithFetch() {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-opus-4-20250514",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: "Explain event loops in Node.js" }],
}),
});
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 the incomplete line for the next chunk
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const payload = line.slice(6).trim();
if (payload === "[DONE]") continue;
const event = JSON.parse(payload);
if (event.type === "content_block_delta") {
process.stdout.write(event.delta.text || "");
}
}
}
}
streamWithFetch();
The key events you'll see in the raw SSE stream:
message_start— the response has begun, includes the initial message shellcontent_block_start— a new content block (text or tool use) is startingcontent_block_delta— incremental text or JSON fragmentscontent_block_stop— the current block finishedmessage_delta— updates to top-level fields likestop_reasonmessage_stop— the stream is complete
Buffering partial lines matters here — chunks from the network don't align with SSE message boundaries, so you need to hold onto an incomplete line until the next read.
Handling errors and disconnects
Streaming requests fail differently than normal requests. A connection can drop mid-stream, or the API can send an error event instead of message_stop. Always:
- Wrap the reader loop in try/catch and close the reader on failure
- Set a timeout if you don't receive any bytes for N seconds
- Check for
event.type === "error"in the parsed payload before assuming success - Log
stop_reason(end_turn,max_tokens,stop_sequence) so you know why generation ended
If you're running Claude access through an API wrapper rather than calling Anthropic directly, confirm the wrapper forwards SSE events unmodified rather than buffering the whole response before sending — otherwise you lose the point of streaming.
Streaming through SubToAPI
If you're exposing Claude access to your own app, internal tools, or a team of developers, calling Anthropic's API directly from every client means managing raw API keys and writing this SSE-handling code in every service. SubToAPI turns your existing Claude access into an HTTPS API with scoped sub_live_... keys, and streaming works the same way shown above — just point your request at https://api.subtoapi.app/v1/messages with stream: true and an Authorization: Bearer $SUBTOAPI_KEY header. The event format matches what's documented in /docs/streaming, so the parsing code in this article works unchanged. Usage metadata per request is also returned, which is handy if you're billing seats or tracking cost by application key — see /docs/messages for the full request/response shape.
Wrapping up
For most Node.js projects, start with the official SDK's .stream() helper — it handles reconnection and parsing for you. Reach for raw fetch and manual SSE parsing when you need full control over the transport, or when you're building a proxy layer yourself. Either way, buffer partial chunks correctly, watch for error events, and always check stop_reason so you know why a generation ended.
questions
Does Claude's API support streaming out of the box? Yes. Set "stream": true in the request body and the API returns a Server-Sent Events stream instead of a single JSON response, with incremental content_block_delta events carrying text.
Can I stream tool use (function calling) responses the same way? Yes, tool use streams through the same event types — content_block_start marks the beginning of a tool call block, and content_block_delta events carry partial JSON for the tool input as it's generated. See /docs/tools for the full event sequence.
What's the fastest way to add streaming without writing SSE parsing code myself? Use the official @anthropic-ai/sdk package's .stream() method, which handles event parsing, reconnection, and gives you a finalMessage() promise for the completed response — no manual buffer handling required.