Server-Sent Events (SSE)

A standard for pushing real-time updates from server to client over HTTP.

Server-Sent Events (SSE) is a web technology that enables servers to push updates to clients over a single HTTP connection. Unlike WebSocket which is bidirectional, SSE is one-way: server to client only. It's perfect for scenarios where you need real-time updates but the client doesn't need to send data back.

SSE vs WebSocket vs Polling

FeatureSSEWebSocketPolling
DirectionServer → ClientBidirectionalClient → Server
ProtocolHTTPWebSocketHTTP
ComplexitySimpleMore complexSimple
Auto-reconnectBuilt-inManualN/A
Binary dataNo (text only)YesYes
Browser supportAll modernAll modernAll

When to Use SSE

Live feeds: News updates, social media timelines, activity streams. Data flows in one direction - from server to user.

Notifications: Real-time alerts, system status updates, progress indicators. User doesn't send anything back, just receives.

Live dashboards: Metrics, analytics, monitoring displays. Data updates automatically without user interaction.

Progress tracking: File upload progress, long-running job status, deployment logs. Server pushes status updates.

AI/LLM streaming: ChatGPT-style responses that stream token by token. SSE is perfect for this - server pushes text chunks, client displays incrementally.

When NOT to Use SSE

Bidirectional communication: Chat apps, multiplayer games - if client needs to send messages frequently, use WebSocket.

Binary data: SSE only supports text. For images, files, or binary protocols, use WebSocket or HTTP.

Many concurrent connections: Each SSE connection holds a TCP connection open. At scale (millions of connections), this can strain servers.

How SSE Works

1. Client opens connection: Client makes GET request with Accept: text/event-stream. Connection stays open.

2. Server sends events: Server writes events to the response stream. Each event has optional id, event type, and data.

3. Auto-reconnect: If connection drops, browser automatically reconnects. Sends Last-Event-ID header so server can resume.

4. Connection stays open: Unlike regular HTTP, the response doesn't end. Server can keep pushing events indefinitely.

Event Format

SSE events are plain text with specific format:

  • data: The message content (required)
  • event: Event type/name (optional)
  • id: Event ID for resuming (optional)
  • retry: Reconnection time in ms (optional)
  • Events separated by blank lines

EventSource API

The browser provides EventSource for consuming SSE:

Connection:

  • new EventSource(url) - Opens connection
  • close() - Closes connection

Events:

  • onopen - Connection established
  • onmessage - Default message received
  • onerror - Error or connection lost
  • addEventListener(type, handler) - Named event types

Properties:

  • readyState - 0 (connecting), 1 (open), 2 (closed)
  • url - Connection URL

Connection Management

Automatic reconnection: SSE reconnects automatically after disconnection. Default is 3 seconds, configurable via retry: field.

Resumption: Send event IDs. On reconnect, browser sends Last-Event-ID header. Server can resume from where it left off.

Heartbeats: Send periodic empty comments (: heartbeat) to keep connection alive through proxies and load balancers.

SSE vs Polling Trade-offs

Why SSE beats polling:

  • No repeated connection overhead
  • Instant updates (no polling interval delay)
  • Less server load (no wasted requests)
  • Simpler client code

When polling might be better:

  • Very infrequent updates (every 5+ minutes)
  • Need to work through aggressive caching proxies
  • Server can't hold connections open

Best Practices

Send event IDs: Always include IDs for reliable resumption. Without IDs, clients miss events during reconnection.

Use event types: Don't put everything in the default message event. Use named types: event: notification, event: update.

Implement heartbeats: Some proxies close idle connections. Send periodic comments to keep the connection alive.

Handle reconnection gracefully: Client will reconnect automatically, but your app state might be stale. Fetch current state on reconnect.

Set appropriate retry interval: Default 3 seconds might be too aggressive. Set retry: 10000 for less critical updates.

Common Mistakes

1. No event IDs: Connection drops, reconnects, client misses events. Always send IDs for resumable streams.

2. Forgetting CORS: SSE is still HTTP. If server and client are on different origins, you need proper CORS headers.

3. No heartbeats: Connection sits idle, proxy kills it, client doesn't know until it's too late. Send periodic heartbeats.

4. Blocking the event loop: On server, if event stream blocks, no other requests are processed. Use non-blocking I/O.

Code Examples

SSE Server and Client

// Server (Node.js/Express)
app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // Send initial connection event
  res.write('event: connected\ndata: {}\n\n');

  let eventId = 0;

  // Send updates every 5 seconds
  const interval = setInterval(() => {
    eventId++;
    res.write(`id: ${eventId}\n`);
    res.write('event: update\n');
    res.write(`data: ${JSON.stringify({ time: new Date() })}\n\n`);
  }, 5000);

  // Heartbeat every 30 seconds
  const heartbeat = setInterval(() => {
    res.write(': heartbeat\n\n');
  }, 30000);

  // Cleanup on disconnect
  req.on('close', () => {
    clearInterval(interval);
    clearInterval(heartbeat);
  });
});

// Client (Browser)
const events = new EventSource('/events');

events.onopen = () => {
  console.log('Connected to event stream');
};

events.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log('Update:', data);
  updateUI(data);
});

events.addEventListener('connected', () => {
  console.log('Stream established');
});

events.onerror = (e) => {
  if (events.readyState === EventSource.CLOSED) {
    console.log('Connection closed');
  } else {
    console.log('Error, will reconnect...');
  }
};