Streaming LLM Responses to the Browser with Server-Sent Events

A
Author
22 Sep, 2026 2 min read 1.07K views

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: no on 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.

Leave a comment

Comments are reviewed before they appear.