Server-Sent Events (SSE) — Streaming Guide

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Server-Sent Events is a simpler alternative to WebSockets for server-to-client communication. Perfect when you only need data flowing one direction.

SSE vs WebSockets

SSE: Simpler, one-way, HTTP-based, built-in reconnection WebSockets: Bidirectional, persistent connection, more complex

Basic SSE Server

import express from "express";

const app = express();

// Keep track of connections
const clients: express.Response[] = [];

app.get("/events", (req, res) => {
  // Set SSE headers
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  // Add to clients list
  clients.push(res);

  // Send a message when client connects
  res.write("data: Connected to server\n\n");

  // Handle client disconnect
  req.on("close", () => {
    const index = clients.indexOf(res);
    if (index > -1) {
      clients.splice(index, 1);
    }
  });
});

// Broadcast to all clients
app.post("/broadcast", (req, res) => {
  const message = req.body.message;

  clients.forEach((client) => {
    client.write(`data: ${message}\n\n`);
  });

  res.json({ sent: clients.length });
});

app.listen(3000, () => {
  console.log("SSE server on port 3000");
});

SSE Message Format

data: message content
event: event-name
id: 1
retry: 1000

data: {"key": "value"}

:this is a comment

data: line 1
data: line 2

Advanced SSE Server

interface ClientEvent {
  id: number;
  name: string;
  data: unknown;
}

let eventId = 0;

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

  const clientId = Date.now();

  // Send initial data
  const events: ClientEvent[] = [
    { id: ++eventId, name: "user-online", data: { clientId } },
    { id: ++eventId, name: "user-count", data: { count: clients.length } },
  ];

  events.forEach((event) => {
    res.write(`id: ${event.id}\n`);
    res.write(`event: ${event.name}\n`);
    res.write(`data: ${JSON.stringify(event.data)}\n\n`);
  });

  clients.push(res);

  // Heartbeat to keep connection alive
  const heartbeat = setInterval(() => {
    res.write(": heartbeat\n\n");
  }, 30000);

  req.on("close", () => {
    clearInterval(heartbeat);
    const index = clients.indexOf(res);
    if (index > -1) {
      clients.splice(index, 1);
    }
  });
});

// Send specific event types
function broadcast(eventName: string, data: unknown) {
  const message = `id: ${++eventId}\nevent: ${eventName}\ndata: ${JSON.stringify(
    data
  )}\n\n`;

  clients.forEach((client) => {
    client.write(message);
  });
}

// Usage
app.post("/notify", (req, res) => {
  broadcast("notification", {
    title: req.body.title,
    message: req.body.message,
  });
  res.json({ ok: true });
});

Client-Side Usage

// Basic usage
const eventSource = new EventSource("http://localhost:3000/events");

eventSource.onmessage = (event) => {
  console.log("Message:", event.data);
};

eventSource.addEventListener("notification", (event) => {
  const data = JSON.parse(event.data);
  console.log("Notification:", data);
});

eventSource.onerror = (error) => {
  console.error("Connection error:", error);
  eventSource.close();
};

// Close connection
eventSource.close();

HTML5 Example

<!DOCTYPE html>
<html>
  <head>
    <title>SSE Demo</title>
  </head>
  <body>
    <h1>Server Messages</h1>
    <div id="messages"></div>

    <script>
      const messagesDiv = document.getElementById("messages");
      const eventSource = new EventSource("/events");

      eventSource.addEventListener("user-count", (event) => {
        const data = JSON.parse(event.data);
        const el = document.createElement("p");
        el.textContent = `Online users: ${data.count}`;
        messagesDiv.appendChild(el);
      });

      eventSource.addEventListener("notification", (event) => {
        const data = JSON.parse(event.data);
        const el = document.createElement("div");
        el.style.padding = "10px";
        el.style.backgroundColor = "#ffd700";
        el.innerHTML = `<strong>${data.title}</strong>: ${data.message}`;
        messagesDiv.appendChild(el);
      });

      eventSource.onerror = () => {
        messagesDiv.innerHTML += "<p>Connection lost</p>";
      };
    </script>
  </body>
</html>

Real-World Examples

Live Notifications

// Backend
const notifications = new Map<string, express.Response[]>();

app.get("/events/:userId", (req, res) => {
  const { userId } = req.params;

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");

  if (!notifications.has(userId)) {
    notifications.set(userId, []);
  }

  notifications.get(userId)!.push(res);

  res.write("data: Connected\n\n");

  req.on("close", () => {
    const clients = notifications.get(userId)!;
    const index = clients.indexOf(res);
    if (index > -1) clients.splice(index, 1);
  });
});

app.post("/notify/:userId", (req, res) => {
  const { userId } = req.params;
  const clients = notifications.get(userId);

  if (clients) {
    clients.forEach((client) => {
      client.write(
        `data: ${JSON.stringify(req.body)}\n\n`
      );
    });
  }

  res.json({ ok: true });
});

// Frontend
const userId = "user-123";
const eventSource = new EventSource(`/events/${userId}`);

eventSource.onmessage = (event) => {
  const notification = JSON.parse(event.data);
  showNotification(notification);
};

Live Analytics

// Backend - send updates every second
app.get("/analytics", (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");

  const interval = setInterval(() => {
    const stats = {
      activeUsers: Math.floor(Math.random() * 1000),
      requests: Math.floor(Math.random() * 10000),
      uptime: process.uptime(),
    };

    res.write(`data: ${JSON.stringify(stats)}\n\n`);
  }, 1000);

  req.on("close", () => {
    clearInterval(interval);
  });
});

// Frontend
const eventSource = new EventSource("/analytics");

eventSource.onmessage = (event) => {
  const stats = JSON.parse(event.data);
  updateDashboard(stats);
};

Error Handling and Reconnection

// Automatic reconnection with exponential backoff
let retryCount = 0;
const maxRetries = 5;
const retryDelay = 1000;

function connectSSE() {
  const eventSource = new EventSource("/events");

  eventSource.onopen = () => {
    retryCount = 0;
    console.log("Connected to server");
  };

  eventSource.onerror = () => {
    eventSource.close();

    if (retryCount < maxRetries) {
      const delay = retryDelay * Math.pow(2, retryCount);
      console.log(`Reconnecting in ${delay}ms`);
      setTimeout(connectSSE, delay);
      retryCount++;
    }
  };
}

connectSSE();

FAQ

Q: When should I use SSE instead of WebSockets? A: When you only need server→client communication and simpler code is preferred. SSE is easier but WebSockets are more flexible.

Q: How many concurrent SSE connections can Node.js handle? A: Thousands to tens of thousands depending on resources. Each connection is lightweight.

Q: Can SSE work through proxies? A: Yes, it uses standard HTTP. Works through any HTTP proxy.


Server-Sent Events are perfect for real-time features where the server pushes data to clients. Simpler than WebSockets and sufficient for many use cases.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro