Best Practices

AI Rules for Server-Sent Events

AI uses WebSocket for simple one-way data push. Rules for EventSource API, SSE vs WebSocket selection, streaming response patterns, automatic reconnection, event typing, and serverless-compatible streaming.

7 min read·March 31, 2025

WebSocket for a notification feed — bidirectional protocol for unidirectional data, manual reconnection

EventSource API, SSE vs WebSocket, auto-reconnect with Last-Event-ID, typed events, LLM streaming

AI Uses WebSocket When SSE Would Suffice

AI generates real-time features with: WebSocket for every use case (even when data only flows server-to-client), complex WebSocket lifecycle management for simple push scenarios, polling as the only alternative to WebSocket (ignoring SSE entirely), no automatic reconnection handling (WebSocket disconnects require manual reconnection logic), and custom message framing (JSON parsing, event typing, error handling all reimplemented). WebSocket is bidirectional — if you only need server-to-client push, it is overkill.

Server-Sent Events (SSE) solve server-to-client push with: native browser API (EventSource — 3 lines of code), automatic reconnection (the browser reconnects and resumes from the last event), built-in event typing (named events without custom framing), HTTP-based (works through proxies, load balancers, and CDNs without special configuration), and streaming response (standard HTTP response that stays open). AI generates none of these when a simple push is needed.

These rules cover: EventSource API usage, SSE vs WebSocket selection criteria, streaming response patterns, automatic reconnection with Last-Event-ID, typed event channels, and serverless-compatible SSE implementations.

Rule 1: Native EventSource API

The rule: 'For server-to-client push, use the EventSource API: const source = new EventSource("/api/events"); source.onmessage = (event) => { const data = JSON.parse(event.data); updateUI(data); }; source.onerror = () => { console.log("Reconnecting..."); }. Three lines for a real-time connection with automatic reconnection, event parsing, and error handling. The server: set Content-Type: text/event-stream, keep the connection open, write events as data: {json}\n\n.'

For the server implementation: 'Express: res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" }); setInterval(() => { res.write("data: " + JSON.stringify({ count: getCount() }) + "\n\n"); }, 1000). Next.js Route Handler: return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }) with a ReadableStream that pushes events. The response stays open; the server writes events as they occur. Standard HTTP — no WebSocket upgrade, no special protocol.'

AI generates: const ws = new WebSocket('ws://...'); ws.onmessage = handler; ws.onclose = () => { setTimeout(() => reconnect(), 1000); }; ws.onerror = handler; — 15+ lines for connection, reconnection, error handling, and message parsing. EventSource: 3 lines. Reconnection is automatic. Event parsing is built-in. Error handling triggers automatic reconnect. Same server-to-client push, 5x less code, zero manual lifecycle management.

  • EventSource: 3 lines of code for real-time server-to-client push
  • Automatic reconnection: browser reconnects and resumes on disconnect
  • Content-Type: text/event-stream — standard HTTP, no protocol upgrade
  • Works through proxies and load balancers without special configuration
  • Built-in event parsing: data, event type, id, retry — no custom framing
💡 3 Lines vs 15+ Lines

WebSocket: 15+ lines for connection, reconnection, error handling, and message parsing. EventSource: new EventSource(url); source.onmessage = handler; — 3 lines. Reconnection is automatic. Parsing is built-in. Same server-to-client push, 5x less code.

Rule 2: SSE vs WebSocket Selection Criteria

The rule: 'Use SSE when: data flows one direction (server to client only), you need automatic reconnection (built into EventSource), your infrastructure is HTTP-based (proxies, CDNs, load balancers work without configuration), and the use case is event notifications (live scores, stock prices, build status, notification feeds). Use WebSocket when: data flows both directions (chat, collaborative editing, gaming), you need binary data (WebSocket supports binary frames, SSE is text-only), or you need sub-10ms latency (WebSocket has less overhead per message than SSE).'

For the common use cases: 'SSE is ideal for: dashboard live updates (metrics refresh every 5 seconds), notification feeds (new notification appears without refresh), build/deploy status (CI pipeline progress streaming), live sports scores (score updates pushed to viewers), stock tickers (price updates streamed), and AI response streaming (LLM token-by-token output — this is how ChatGPT streams responses). WebSocket is ideal for: chat (bidirectional messages), collaborative editing (CRDT operations in both directions), multiplayer games (inputs and state both directions).'

AI generates: WebSocket for a notification feed (server pushes, client never sends — bidirectional protocol for unidirectional data). The WebSocket requires: custom reconnection logic, heartbeat/ping-pong, and protocol upgrade support from infrastructure. SSE: automatic reconnection, standard HTTP, and the browser handles the protocol entirely. WebSocket for notifications is like using a phone call when a text message would do — it works, but it is more complex than necessary.

Rule 3: Automatic Reconnection with Last-Event-ID

The rule: 'SSE reconnects automatically on disconnect. The browser waits for the retry interval (configurable server-side: retry: 3000\n = 3 seconds), then reconnects. On reconnect: the browser sends the Last-Event-ID header (the id of the last received event). The server: checks Last-Event-ID, replays events since that ID. The client receives missed events and is caught up — no gaps, no manual state management. Server: id: 42\ndata: {...}\n\n — the 42 becomes the Last-Event-ID on reconnect.'

For the server-side resume: 'Store events in a buffer (last 1000 events) or in a database with sequential IDs. On connection with Last-Event-ID: send all events with ID > Last-Event-ID, then continue with live events. On connection without Last-Event-ID (first connect): send the current state or the last N events for context, then continue with live events. The buffer size determines how long a disconnect can last without losing events. 1000 events at 1/second = 16 minutes of catch-up buffer.'

AI generates: WebSocket with manual reconnection: ws.onclose = () => { setTimeout(() => { ws = new WebSocket(url); }, 3000); }. No Last-Event-ID. No event replay. The client misses all events during the disconnect. With SSE: reconnection is automatic, Last-Event-ID is sent automatically, the server replays missed events automatically. Zero client code for reconnection, zero missed events.

  • Auto-reconnect: browser handles reconnection with configurable retry interval
  • Last-Event-ID header: sent automatically on reconnect, server replays missed events
  • Server id field: id: 42\n sets the client Last-Event-ID to 42
  • retry field: retry: 3000\n sets reconnection interval to 3 seconds
  • Event buffer: last 1000 events for catch-up on reconnect — no gaps
⚠️ Zero Missed Events on Reconnect

WebSocket disconnect: manual reconnect, all events during disconnect are lost. SSE disconnect: browser reconnects automatically, sends Last-Event-ID header, server replays missed events. Zero client code for reconnection, zero gaps in the event stream.

Rule 4: Typed Event Channels

The rule: 'Use named event types to multiplex different data on one SSE connection. Server: event: notification\ndata: {...}\n\n and event: metric\ndata: {...}\n\n. Client: source.addEventListener("notification", handler); source.addEventListener("metric", handler). Each event type has its own handler. The default onmessage handler receives events without an event type. Named events: one connection carries multiple data streams, each processed by the appropriate handler.'

For multiplexing: 'A dashboard SSE connection carries: notification events (new notification badge), metric events (chart data points), status events (system health indicators), and alert events (critical alerts with different UI treatment). One EventSource connection, four event types, four handlers. Without named events: every handler receives every event and must check the type manually. With named events: the browser routes each event to the correct handler automatically — built-in event routing.'

AI generates: one WebSocket connection with JSON messages like { type: 'notification', data: {...} }. The client: switch (msg.type) { case 'notification': handleNotification(msg); break; case 'metric': handleMetric(msg); break; }. Custom routing logic. With SSE named events: source.addEventListener('notification', handler) — the browser does the routing. No switch statement, no type checking, no custom message framing. The protocol handles it.

Rule 5: Serverless-Compatible SSE Patterns

The rule: 'SSE works in serverless environments (Vercel Edge Functions, Cloudflare Workers, AWS Lambda with streaming) because it is standard HTTP — no WebSocket upgrade needed. Next.js App Router: export async function GET() { const stream = new ReadableStream({ start(controller) { const encoder = new TextEncoder(); sendEvent = (data) => controller.enqueue(encoder.encode("data: " + JSON.stringify(data) + "\n\n")); } }); return new Response(stream, { headers: { "Content-Type": "text/event-stream" } }); }. The ReadableStream stays open, events are enqueued as they occur.'

For AI response streaming: 'LLM token-by-token streaming (how ChatGPT works): the API generates tokens incrementally, each token is sent as an SSE event, and the client renders tokens as they arrive. Pattern: fetch the LLM API with streaming enabled, pipe the token stream to the SSE response. The user sees words appearing in real-time instead of waiting 5 seconds for the complete response. SSE is the standard transport for LLM streaming — OpenAI, Anthropic, and all major AI APIs use it.'

AI generates: a non-streaming API response (wait for the entire LLM output, then send). User waits 5-10 seconds staring at a loading spinner, then the full response appears at once. With SSE streaming: the first token appears in 200ms, words flow continuously, and the user reads as the response generates. Same total time, dramatically better perceived performance. SSE makes the generation process visible and interactive.

ℹ️ First Token in 200ms, Not 5 Seconds

Non-streaming LLM response: wait 5-10 seconds, then full text appears at once. SSE streaming: first token in 200ms, words flow continuously as they generate. Same total time, dramatically better perceived performance. This is how ChatGPT works — SSE is the standard AI streaming transport.

Complete Server-Sent Events Rules Template

Consolidated rules for Server-Sent Events.

  • EventSource API: 3 lines for real-time push — automatic reconnection, built-in parsing
  • SSE for one-way push (notifications, metrics, streaming). WebSocket for bidirectional (chat, collab)
  • Last-Event-ID: automatic resume on reconnect, server replays missed events
  • Named event types: multiplex notification, metric, alert on one connection
  • Standard HTTP: works through proxies, CDNs, load balancers without special config
  • Serverless-compatible: ReadableStream in Next.js, Edge Functions, Cloudflare Workers
  • LLM streaming: token-by-token delivery via SSE — OpenAI and Anthropic standard transport
  • retry field: server controls reconnection interval — client handles automatically