I’m trying to integrate the Anthropic Claude API into my Node.js application using the official @anthropic-ai/sdk package. I’m using the client.messages.stream method to handle responses as they come in.
The issue is that my Node.js process seems to hang after the initial chunk of the response is received, and the on('end') event never fires. The server just sits there until it eventually times out.
Here is the relevant code snippet from my Node.js server (using Express):
import Anthropic from '@anthropic-ai/sdk';
import express from 'express';
const app = express();
// ANTHROPIC_API_KEY is set in my .env file and picked up correctly
const client = new Anthropic();
app.post('/stream-claude', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
const messageStream = await client.messages.stream({
model: 'claude-3-5-sonnet-20240620',
messages: [{ role: 'user', content: 'Why is the ocean salty? Give a detailed explanation.' }],
max_tokens: 1000,
});
for await (const chunk of messageStream) {
if (chunk.type === 'content_block_delta') {
// Sending each text chunk as a Server-Sent Event (SSE)
res.write(`data: ${JSON.stringify({ text: chunk.delta.text })}\n\n`);
}
}
// The process seems to hang before reaching here
res.write(`data: ${JSON.stringify({ event: 'end' })}\n\n`);
res.end(); // This line is never reached
console.log('Stream ended successfully.');
} catch (error) {
console.error('Error during streaming:', error);
res.status(500).end('Internal Server Error');
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
Expected Behavior:
The client receives a series of Server-Sent Events (SSE) with the text chunks, followed by an end event, and the Node.js process completes the request.
Actual Behavior:
The client receives the text chunks, but the connection remains open indefinitely, and the console.log('Stream ended successfully.'); is never printed.
What I’ve tried:
- Verified my API key and network connectivity.
- Tried running without Express, using only native Node.js http, with the same result.
- Checked the Anthropic Node.js SDK documentation, but it only shows console logging the stream, not integrating it with an HTTP response.
Any ideas on how to properly terminate the stream and the HTTP response?
You’re not actually “hung” — you’re waiting on a stream that never finishes from your side.
This is a subtle but common mistake when using the Anthropic streaming API with for await (...) and piping it into SSE.
Root cause
client.messages.stream() does not automatically terminate the async iterator when the model finishes generating text.
Instead, the stream emits a final control event (message_stop) and then remains open until you explicitly finish consuming it or call one of the helper termination methods.
Because your loop never breaks, this line is never reached:
res.end();
So Express keeps the connection alive indefinitely.
What’s actually happening under the hood
Anthropic streams multiple event types, not just content:
-
content_block_delta→ text tokens -
message_stop→ model is done -
error/ping/metadata
Your loop only handles content_block_delta, but it never stops when generation is complete.
Correct way to terminate the stream
Option 1 (recommended): Break on message_stop
Explicitly detect the stop signal and exit the loop.
Option 2: Await finalMessage() (cleanest API-wise)
Anthropic provides a helper that drains the stream properly:
const messageStream = await client.messages.stream({ ... });
for await (const chunk of messageStream) {
if (chunk.type === 'content_block_delta') {
res.write(`data: ${JSON.stringify({ text: chunk.delta.text })}\n\n`);
}
}
await messageStream.finalMessage(); // <-- important
res.end();
Without this call, the iterator may never resolve.
SSE-specific best practices (you are missing one)
You should flush headers immediately, otherwise some proxies will buffer indefinitely:
res.flushHeaders?.();
Also consider sending keep-alive pings for long responses:
res.write(': keep-alive\n\n');
Why the SDK docs don’t show this
The docs demonstrate console streaming, not lifecycle-managed HTTP streaming. Console consumers don’t care if the process stays alive; servers do.
This is one of those “works in a demo, fails in production” traps.
Summary (TL;DR)
-
The stream does not auto-close
-
You must:
-
break on
message_stop, or -
call
finalMessage()
-
-
Otherwise, your async iterator never completes
-
Express is behaving correctly
Your code is fundamentally sound — the missing termination logic is the problem.
If you’re planning to productionize this, I strongly recommend wrapping this logic into a reusable stream adapter and adding explicit timeouts and abort signals.
