Claude API Streaming Response in Node.js: Full Guide
If you're building a chat interface, a CLI tool, or any product that needs to show Claude's response token-by-token instead of waiting for the full reply, you need streaming. In Node.js, streaming a Claude API response means consuming a server-sent events (SSE) connection and writing text chunks to your UI, terminal, or HTTP response as they arrive, rather than buffering the entire completion before doing anything with it.
This matters for two practical reasons: perceived latency and memory. A non-streaming request to a model generating a few thousand tokens can take 10-30 seconds before you see anything. Streaming gets the first tokens to the user in under a second. It also lets you process long outputs incrementally instead of holding a huge string in memory until the model finishes.
How streaming actually works
Claude's API (and compatible gateways like SubToAPI) stream responses using SSE over a standard HTTP connection. Instead of one JSON blob, the server sends a sequence of events — each one a small JSON chunk — terminated by a final event marking completion. Your Node.js client reads these events off the response stream as they arrive and appends the text deltas to build the full message.
The key technical point: this is not WebSockets, and you don't need a WebSocket library. It's a regular HTTPS request where the response body is kept open and fed to you incrementally. Node's built-in fetch (Node 18+) or https module handles this natively — no extra dependencies required for the transport layer, though the official Anthropic SDK and SubToAPI's client wrap the parsing for you.
Streaming with fetch and async iteration
The cleanest way to consume a stream in modern Node.js is with the Web Streams API, which fetch returns by default:
const response = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
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 incomplete line for next chunk
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = line.slice(6);
if (payload === '[DONE]') continue;
const event = JSON.parse(payload);
if (event.type === 'content_block_delta') {
process.stdout.write(event.delta.text);
}
}
}
A few things to note here that trip people up:
- SSE lines arrive in chunks, not whole events. A single
read()call might give you half adata:line. You have to buffer and split on newlines, keeping the trailing incomplete fragment for the next iteration — that's whatbuffer = lines.pop()does above. - Event types vary. You'll see
message_start,content_block_start,content_block_delta,content_block_stop, andmessage_stop. The actual text lives incontent_block_deltaevents underdelta.text. - Always handle
[DONE]or the final stop event explicitly so you don't try to parse an empty or non-JSON payload.
Streaming to an HTTP response in Express
If you're building an API endpoint that proxies Claude's stream to a browser client, pipe the chunks through as SSE yourself:
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://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
stream: true,
messages: req.body.messages
})
});
const reader = upstream.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(decoder.decode(value, { stream: true }));
}
res.end();
});
This forwards raw SSE straight to the browser, where EventSource or a fetch reader on the frontend can consume it the same way. Note that res.flushHeaders() or disabling compression middleware (compression()) for this route may be necessary — gzip buffering can silently defeat streaming and make your "streamed" response arrive all at once anyway.
Common mistakes that break streaming
A few issues show up repeatedly when people implement this:
- Reverse proxies buffering the response. Nginx, some CDNs, and certain hosting platforms buffer responses by default. You need
X-Accel-Buffering: no(Nginx) or equivalent to disable it, or the stream degrades into one big chunk at the end. - Forgetting
stream: truein the request body. Without it, the API returns a normal JSON response and none of your SSE parsing code runs. - Not handling connection drops. Long-running streams can be cut by load balancers with aggressive idle timeouts. Implement a client-side retry that resumes gracefully, or at minimum surface a clear error instead of hanging silently.
- Parsing JSON greedily. Splitting on newlines and parsing each
data:line individually, as shown above, avoids partial-JSON parse errors that happen when you try toJSON.parse()an entire buffer that hasn't fully arrived yet.
Using a managed gateway instead
Implementing SSE parsing, reconnect logic, and buffer handling correctly is doable but easy to get subtly wrong, especially under load with many concurrent streams. If you want the API shape without managing the streaming plumbing yourself, SubToAPI turns your existing Claude access into an HTTPS API with sub_live_... keys, and exposes the same streaming behavior described above through a standard endpoint — see the streaming docs and the quickstart for a working example. It also gives you usage metadata per request and team seats if multiple people or services need access, which is useful once streaming moves from a prototype into something customer-facing. Plans start at pricing, with a free trial at signup.
Questions
Does streaming reduce total response time for Claude API calls? No — total generation time is roughly the same. Streaming reduces perceived latency by showing the first tokens almost immediately instead of making the user wait for the entire response to finish generating.
Can I use WebSockets instead of SSE for Claude streaming in Node.js? Claude's API streams over SSE, not WebSockets. You can wrap an SSE stream inside a WebSocket on your own backend if you need bidirectional messaging, but the upstream connection to Claude itself is SSE-based.
Why does my Node.js stream arrive all at once instead of incrementally? This is almost always a buffering issue — either compression middleware, a reverse proxy, or CDN buffering the response before forwarding it. Disable buffering for that route/path and confirm stream: true is set in the request body.