Streaming LLM Responses to the Browser with Server-Sent Events
Long answers can take many seconds to generate. Streaming shows text as it is produced, so users see progress immediately instead of staring at a spinner.
Streaming on the server
The SDK's messages.stream() helper gives you an async iterator of events plus a text event for just the new text.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const stream = client.messages.stream({
model: 'claude-opus-5-5',
max_tokens: 64000,
messages: [{ role: 'user', content: 'Write a short guide to Git branches.' }],
});
stream.on('text', (delta) => process.stdout.write(delta));
const final = await stream.finalMessage(); // full Message once the stream ends
console.log('\nOutput tokens:', final.usage.output_tokens);
Forwarding the stream with Express and SSE
Server-Sent Events are a simple, one-way HTTP stream that browsers support natively. Each message is a data: line followed by a blank line.
import express from 'express';
import Anthropic from '@anthropic-ai/sdk';
const app = express();
const client = new Anthropic();
app.use(express.json());
app.post('/api/chat', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = client.messages.stream({
model: 'claude-opus-5-5',
max_tokens: 64000,
messages: req.body.messages,
});
// Stop generating (and paying) if the user closes the tab
req.on('close', () => stream.abort());
stream.on('text', (delta) => res.write(`data: ${JSON.stringify({ delta })}\n\n`));
try {
await stream.finalMessage();
res.write('data: [DONE]\n\n');
} catch (err) {
res.write(`data: ${JSON.stringify({ error: 'Generation failed' })}\n\n`);
}
res.end();
});
Reading the stream in the browser
EventSource only supports GET, so for a POST body read the response stream with fetch:
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages }),
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const events = buffer.split('\n\n');
buffer = events.pop(); // keep any partial event for the next chunk
for (const event of events) {
const data = event.replace(/^data: /, '');
if (data === '[DONE]') return;
output.textContent += JSON.parse(data).delta ?? '';
}
}
Tips
- Use streaming for any request with a large
max_tokens; it also avoids HTTP timeouts on long generations. - Disable proxy buffering (for example
X-Accel-Buffering: noon Nginx) or tokens will arrive in one burst. - Render Markdown after each chunk is appended, not per token, to keep the UI smooth.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.