Skip to content

Server-Sent Events pour streaming LLM #269

Description

@khalilbenaz

lede: SSE est le pattern défaut pour streaming réponses LLM en 2026. Plus simple que WebSocket, idéal token-by-token.
lede_en: SSE is the default pattern for streaming LLM responses in 2026. Simpler than WebSocket, ideal token-by-token.
title_en: Server-Sent Events for LLM streaming

Pourquoi SSE pour LLM

Streaming LLM responses (token-by-token) améliore l'UX énormément. User voit la réponse arriver en temps réel plutôt que d'attendre 5s.

Options techniques :

  • WebSocket : bidirectionnel, mais overhead inutile pour streaming server→client.
  • SSE (Server-Sent Events) : unidirectionnel server→client, HTTP/2 natif.

SSE est le bon outil pour streaming LLM.

Implémentation côté serveur

// Hono / Express
app.post('/chat', async (c) => {
  return c.streamSSE(async (stream) => {
    const llmStream = await anthropic.messages.stream({
      model: 'claude-sonnet-4-6',
      messages: [...],
      max_tokens: 1024,
    });

    for await (const event of llmStream) {
      if (event.type === 'content_block_delta') {
        await stream.writeSSE({ data: event.delta.text });
      }
    }
  });
});

OpenAI/Anthropic/Gemini SDK exposent tous des streams. Vous relayez vers SSE.

Côté client

const response = await fetch('/chat', {
  method: 'POST',
  body: JSON.stringify({ message: 'Hello' }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  // Parse SSE format: "data: <token>

"
  for (const line of chunk.split('

')) {
    if (line.startsWith('data: ')) {
      const token = line.slice(6);
      appendToUI(token);
    }
  }
}

Plus simple qu'EventSource pour POST (EventSource ne supporte que GET).

Vs WebSocket

SSE WebSocket
Direction Server → Client Bidirectionnel
Protocole HTTP normal WS protocol
Reconnect Auto Manuel
Proxies Compatible Parfois bloqués
Use case Streaming Chat collab

Pour LLM streaming : SSE clairement.

Patterns avancés

Cancellation :

const controller = new AbortController();
fetch('/chat', { signal: controller.signal });

// User clicks stop
controller.abort();

Côté serveur, détecter abort et stopper LLM stream :

c.req.raw.signal.addEventListener('abort', () => {
  llmStream.controller.abort();
});

Économise tokens.

Heartbeat :

// Ping every 15s pour keep-alive
setInterval(() => stream.writeSSE({ event: 'ping' }), 15000);

Évite proxy timeouts.

Limites

  • Pas pour bidirectionnel : si user doit envoyer pendant que LLM stream, WebSocket mieux.
  • HTTP/1.1 limit : 6 connections par domain. Use HTTP/2 ou multiplex.

Verdict

Pour streaming LLM responses : SSE est standard 2026. Plus simple qu'WebSocket, suffisant pour 95 % des cas.

Why SSE for LLM

Streaming LLM responses (token-by-token) hugely improves UX. User sees response arriving in real time instead of waiting 5s.

Technical options:

  • WebSocket: bidirectional, but unnecessary overhead for server→client streaming.
  • SSE (Server-Sent Events): unidirectional server→client, native HTTP/2.

SSE is the right tool for LLM streaming.

Server-side implementation

// Hono / Express
app.post('/chat', async (c) => {
  return c.streamSSE(async (stream) => {
    const llmStream = await anthropic.messages.stream({
      model: 'claude-sonnet-4-6',
      messages: [...],
      max_tokens: 1024,
    });

    for await (const event of llmStream) {
      if (event.type === 'content_block_delta') {
        await stream.writeSSE({ data: event.delta.text });
      }
    }
  });
});

OpenAI/Anthropic/Gemini SDKs all expose streams. You relay to SSE.

Client-side

const response = await fetch('/chat', {
  method: 'POST',
  body: JSON.stringify({ message: 'Hello' }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  const chunk = decoder.decode(value);
  // Parse SSE format: "data: <token>

"
  for (const line of chunk.split('

')) {
    if (line.startsWith('data: ')) {
      const token = line.slice(6);
      appendToUI(token);
    }
  }
}

Simpler than EventSource for POST (EventSource only supports GET).

vs WebSocket

SSE WebSocket
Direction Server → Client Bidirectional
Protocol Normal HTTP WS protocol
Reconnect Auto Manual
Proxies Compatible Sometimes blocked
Use case Streaming Collab chat

For LLM streaming: SSE clearly.

Advanced patterns

Cancellation:

const controller = new AbortController();
fetch('/chat', { signal: controller.signal });

// User clicks stop
controller.abort();

Server-side, detect abort and stop LLM stream:

c.req.raw.signal.addEventListener('abort', () => {
  llmStream.controller.abort();
});

Saves tokens.

Heartbeat:

// Ping every 15s for keep-alive
setInterval(() => stream.writeSSE({ event: 'ping' }), 15000);

Avoids proxy timeouts.

Limits

  • Not for bidirectional: if user must send while LLM streams, WebSocket better.
  • HTTP/1.1 limit: 6 connections per domain. Use HTTP/2 or multiplex.

Verdict

For streaming LLM responses: SSE is 2026 standard. Simpler than WebSocket, enough for 95% of cases.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions