← Back to DevBytes

Implementing SSE Protocol: From Theory to Practice

What is Server-Sent Events (SSE)?

Server-Sent Events (SSE) is a standardized technology that allows a server to push real-time updates to a client over a single, long-lived HTTP connection. Unlike WebSockets, which enable bidirectional communication, SSE is strictly unidirectional β€” data flows only from server to client. The client opens a connection using the browser’s built-in EventSource API, and the server keeps the response open, streaming specially formatted text as events arrive.

The protocol rides on top of standard HTTP (or HTTP/2) and uses the text/event-stream MIME type. Because it is simple HTTP, SSE works effortlessly through proxies, load balancers, and existing infrastructure that already understands HTTP semantics. The event stream format is lightweight: each message consists of one or more data: lines, an optional event: line to name the event type, and an optional id: line for resuming. Messages are separated by a blank line.

Why SSE Matters

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

SSE shines in scenarios where the server needs to continuously push data to the client, but the client rarely needs to send data back (beyond the initial request). This covers a wide range of practical use cases:

Compared to alternatives like polling (wasteful, high latency) or WebSockets (complex upgrade handshake, bidirectional overhead), SSE offers a sweet spot: minimal client code, automatic reconnection, built-in event IDs for resuming, and zero dependency on external libraries. It leverages the same HTTP/2 multiplexing benefits, allowing many concurrent streams over a single connection. For server-to-client streaming, SSE is often the most pragmatic choice.

How SSE Works: The Protocol

An SSE transaction starts with a standard HTTP GET request. The client must include the Accept: text/event-stream header to signal its capability. The server responds with HTTP status 200, sets the Content-Type to text/event-stream, disables caching via Cache-Control: no-cache, and keeps the connection open using Connection: keep-alive (or implicitly via HTTP/2).

Once the connection is established, the server writes event stream messages. Each message can contain the following fields, defined by the specification:

Lines beginning with a colon (:) are comments and are ignored by clients, often used as keep-alive pings to prevent intermediate proxies from closing the connection. A blank line signals the end of a message. Here is an example raw stream:

: this is a comment
event: stock-update
data: {"symbol":"AAPL", "price": 174.50}
id: 42

data: initial heartbeat

event: alert
data: {"severity":"high", "message":"CPU over 90%"}
id: 43
retry: 3000

Implementing SSE: Server Side (Node.js with Express)

Let's build a minimal SSE server using Express. The key is to keep the response writable and never call res.end() until the client disconnects. We disable response compression because compressed streams cannot be flushed incrementally; each chunk must be sent immediately.

const express = require('express');
const app = express();

// Store active SSE connections
const clients = new Set();

app.get('/events', (req, res) => {
  // Set headers for SSE
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    // Optional: disable compression middleware if present
    'X-Accel-Buffering': 'no',   // for Nginx
  });

  // Send initial comment to open the stream
  res.write(':ok\n\n');

  // Add client to set
  const clientId = Date.now();
  clients.add(res);
  console.log(`Client connected (${clients.size} total)`);

  // Send a welcome event
  res.write(`event: connected\ndata: {"clientId":${clientId}}\n\n`);

  // Clean up on client disconnect
  req.on('close', () => {
    clients.delete(res);
    console.log(`Client disconnected (${clients.size} remaining)`);
  });
});

// Example: broadcast to all clients every 5 seconds
setInterval(() => {
  const timestamp = new Date().toISOString();
  const message = `data: ${JSON.stringify({ time: timestamp })}\n\n`;
  for (const client of clients) {
    client.write(message);
  }
}, 5000);

// Keep-alive pings every 15 seconds to prevent proxy timeouts
setInterval(() => {
  for (const client of clients) {
    client.write(':ping\n\n');
  }
}, 15000);

app.listen(3000, () => {
  console.log('SSE server running on http://localhost:3000');
});

Notice how we store each response object (res) in a Set. This allows broadcasting to all connected clients. The req.on('close') handler ensures we remove disconnected clients to prevent memory leaks. The keep-alive ping (:ping) is a comment line that keeps the connection alive through proxies and load balancers without triggering client events.

Handling Custom Events and IDs

To send a named event with an ID for resuming, we simply prepend the appropriate fields:

function sendEvent(res, eventName, data, id) {
  const lines = [];
  if (eventName) lines.push(`event: ${eventName}`);
  lines.push(`data: ${JSON.stringify(data)}`);
  if (id !== undefined) lines.push(`id: ${id}`);
  lines.push(''); // blank line terminates message
  res.write(lines.join('\n') + '\n');
}

Resuming from Last Event ID (Server-side)

When a client reconnects, it sends the Last-Event-ID HTTP header. We can read it from req.headers['last-event-id'] and replay missed events (assuming we stored them in a database or queue). For simplicity, we can just acknowledge the ID and continue streaming. The following snippet shows a basic approach:

app.get('/events', (req, res) => {
  const lastId = req.headers['last-event-id'] || '0';
  console.log(`Client reconnected, last event ID: ${lastId}`);
  // In a real app, fetch missed events from a store and send them
  // For now, just set up streaming as before...
  res.writeHead(200, { /* headers */ });
  res.write(`event: resumed\ndata: {"lastId":"${lastId}"}\n\n`);
  // ... continue streaming
});

Implementing SSE: Client Side (JavaScript)

On the client, the EventSource interface handles everything. Instantiate it with the server URL, and attach listeners. The browser automatically parses the stream, fires events, and reconnects on failure with exponential backoff (or using the retry field).

const eventSource = new EventSource('http://localhost:3000/events');

// Generic message handler (no event field)
eventSource.onmessage = (e) => {
  console.log('Generic message:', e.data);
};

// Named event handlers
eventSource.addEventListener('stock-update', (e) => {
  const stock = JSON.parse(e.data);
  console.log(`Stock update: ${stock.symbol} $${stock.price}`);
});

eventSource.addEventListener('alert', (e) => {
  const alert = JSON.parse(e.data);
  console.warn(`Alert (${alert.severity}): ${alert.message}`);
});

// Connection opened
eventSource.onopen = () => {
  console.log('SSE connection established');
};

// Error handling (including reconnection attempts)
eventSource.onerror = (e) => {
  // The browser will attempt to reconnect automatically
  console.error('SSE error, readyState:', eventSource.readyState);
  // readyState: 0=CONNECTING, 1=OPEN, 2=CLOSED
  // If the server set a retry field, the browser uses it; otherwise backoff.
};

// To manually close the connection (e.g., user navigates away)
// eventSource.close();

The EventSource API automatically sends the Accept: text/event-stream header and handles Last-Event-ID on reconnect. It also respects the retry field from the server, overriding the default backoff algorithm. When the connection drops, it transitions to CONNECTING state and retries indefinitely β€” you rarely need to manage reconnection yourself.

Best Practices

Conclusion

Server-Sent Events provide a remarkably straightforward, standards-based mechanism for real-time server-to-client streaming. By sticking to plain HTTP and a minimal text format, SSE eliminates the complexity of bidirectional protocols while delivering automatic reconnection, event resuming, and trivial client implementation. For any application where data predominantly flows from server to client β€” dashboards, notifications, log feeds, progress updates β€” SSE should be your first consideration. Following the patterns above, you can build a robust, scalable SSE infrastructure that coexists seamlessly with your existing HTTP stack and delivers instantaneous updates with minimal overhead.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles