Search IOCombats

Search challenges, guides, questions and articles

SSE vs WebSockets: which one suits streaming LLM responses, and why?

Intermediate8 min interview
Skills tested:
Comparing SSE and WebSockets on direction, protocol and infrastructure costKnowing that major LLM APIs stream over SSE and why that fits request-response generationKnowing the limits of the EventSource API (GET only, no custom headers)Reading an SSE stream from a POST request with fetchRecognising when a bidirectional transport like WebSockets is actually needed

Advertisement

🧩 Scenario

You are adding a chat assistant to a Next.js dashboard. A teammate wants to set up a WebSocket server so answers can stream in. The interviewer asks whether that is the right call and what you would use instead.

Concept Walkthrough

What each transport actually is

Server-Sent Events is just an HTTP response that never ends quickly. The server sets Content-Type: text/event-stream and keeps writing small text blocks like data: {...} followed by a blank line. The client reads them as they arrive. Data flows one way: server to client. If you have ever used res.write() in Express without calling res.end(), you already understand the core idea.

WebSockets start as an HTTP request, then upgrade to a separate protocol. After the upgrade, both sides can send messages at any time over the same connection. It is a two-way pipe, closer to a phone call than a letter.

Why LLM text streaming fits SSE

A chat completion is a request-response operation. The client sends the prompt and history once, and the model generates tokens one after another. Nothing needs to flow back from the client while the answer is being written, except "stop", and you can do that by aborting the HTTP request. So SSE matches the shape of the work.

That is why the providers use it. OpenAI documents streaming as HTTP with stream: true over SSE, emitting typed events such as response.output_text.delta. Anthropic streams Messages as SSE events such as message_start, content_block_delta and message_stop.

SSE also fits the infrastructure frontend teams already have. It is plain HTTP, so cookies, Authorization headers, CORS, HTTP/2 and Route Handlers all work as usual. WebSockets need a server that keeps connections open, which many serverless setups are not designed for.

The EventSource catch

The browser's EventSource API can only make a GET request, and its only option is withCredentials. A chat request needs a POST body with messages and often an auth header. So in practice you send a POST with fetch and parse the SSE format from response.body yourself, or let an SDK do it.

When WebSockets win

Use WebSockets when the client keeps sending data during the session: realtime voice where audio flows both ways, a user interrupting while the model speaks, or a persistent agent session that receives new inputs without reconnecting. OpenAI, for example, documents a separate WebSocket mode for persistent connections alongside its SSE streaming.


Key Code Explained

// app/api/chat/route.ts (Next.js Route Handler)
// SSE is just a streaming HTTP response with the right content type.
export async function POST(req: Request) {
  const { prompt } = (await req.json()) as { prompt: string };
  const encoder = new TextEncoder();

  const stream = new ReadableStream<Uint8Array>({
    async start(controller) {
      // In a real app these tokens come from the LLM provider.
      for (const token of ['Hello', ' from', ' SSE']) {
        // One SSE event = "data: ..." followed by a blank line.
        controller.enqueue(encoder.encode(`data: ${JSON.stringify({ token })}\n\n`));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
    },
  });
}
// Client: POST with fetch, because EventSource cannot send a body.
async function streamChat(prompt: string, onToken: (t: string) => void) {
  const res = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt }),
  });
  if (!res.ok || !res.body) throw new Error(`Request failed: ${res.status}`);

  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = '';

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;

    // Events are separated by a blank line. Keep any partial event in the buffer.
    let sep: number;
    while ((sep = buffer.indexOf('\n\n')) !== -1) {
      const rawEvent = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      for (const line of rawEvent.split('\n')) {
        if (line.startsWith('data:')) {
          const { token } = JSON.parse(line.slice(5).trim()) as { token: string };
          onToken(token);
        }
      }
    }
  }
}

The server side is ordinary Web API code: a ReadableStream passed to new Response(). The client keeps a string buffer because one network chunk can hold half an event or several events. Parsing only complete events (up to the blank line) is the rule that keeps this correct.


Tradeoffs

AspectSSEWebSockets
DirectionServer to client onlyBoth directions
ProtocolPlain HTTP responseHTTP upgrade, then its own protocol
Auth, cookies, CORSWorks like any fetchNeeds its own handling at connect time
Serverless friendlinessGood, as long as the platform supports streaming responsesNeeds long-lived connections the platform may not keep
Browser APIEventSource (GET only) or fetch + parsingWebSocket
Best LLM use caseText chat, completions, tool call progressRealtime voice, interruptible sessions, persistent agent sessions

What Interviewers Actually Check

  • Whether you can explain the one-way vs two-way difference in plain terms
  • Whether you know the main LLM APIs stream over SSE and can explain why
  • Whether you know EventSource cannot send a POST body or custom headers
  • Whether you think about infrastructure (serverless, proxies, auth) and not just the API
  • Whether you can name a case where WebSockets are justified instead of dismissing them

Follow-Up Questions

  1. How would you implement a "stop generating" button over SSE without a second channel?
  2. What does the id field in an SSE event do, and why do most LLM chat UIs not rely on automatic reconnection?
  3. How do HTTP/1.1 connection limits affect a user with several tabs open, and how does HTTP/2 change that?
  4. What would you need to change in a Next.js app on a serverless host to support WebSockets?
  5. How would you stream tool call progress and text in the same SSE response?

Common Candidate Mistakes

  • Choosing WebSockets for plain text chat and then fighting serverless timeouts and scaling
  • Saying "SSE is old, WebSockets are modern" instead of comparing the actual shape of the data flow
  • Trying to send the chat history as query parameters so EventSource can be used
  • Parsing each network chunk as if it were exactly one event
  • Forgetting Cache-Control: no-cache, letting an intermediate cache or proxy interfere with the stream

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you explain SSE and WebSockets in two sentences each?
  • Can you say which transport the major LLM APIs use for streaming text?
  • Can you explain why a chat UI usually uses fetch instead of EventSource?
  • Can you write the SSE wire format from memory?
  • Can you name a feature that would push you to WebSockets?

Summary

SSE is a long-running HTTP response that pushes text events from server to client. WebSockets are a two-way connection after an HTTP upgrade. Streaming an LLM answer is one request followed by many server-to-client chunks, so SSE fits it, and that is what OpenAI and Anthropic use. It also works with the HTTP tooling you already have: Route Handlers, cookies, auth headers and HTTP/2. Because EventSource only supports GET, chat UIs usually POST with fetch and parse the event stream from response.body. Reach for WebSockets when the client must keep sending data during the session, like realtime voice or interruptible agents.

Frequently Asked Questions

Do OpenAI and Anthropic stream over SSE?

Yes. OpenAI documents HTTP streaming with stream set to true over server-sent events, with typed events such as response.output_text.delta. Anthropic streams Messages over SSE with events such as message_start, content_block_delta and message_stop.

Can I use the browser EventSource API for an LLM chat endpoint?

Usually not directly. EventSource always makes a GET request and its only option is withCredentials, so you cannot send a JSON body or an Authorization header. Most chat UIs send a POST with fetch and parse the SSE text from response.body themselves.

Is SSE limited to 6 connections?

Only over HTTP/1.1, where browsers cap connections per domain at a low number (MDN says 6, shared across tabs). Over HTTP/2 the number of concurrent streams is negotiated and defaults to 100, so this is rarely a problem on modern hosting.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Namaste Dev
Sponsored

Cohort-style courses to go from tutorials to job-ready.

Explore Courses

Advertisement