hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Real-Time APIs with @hectoday/http

Beyond Request-Response

  • Why Real-Time Matters
  • Project Setup

Polling

  • Short Polling
  • Long Polling

Server-Sent Events

  • How SSE Works
  • Building an SSE Endpoint
  • Event Types and IDs
  • SSE in Practice

WebSockets

  • How WebSockets Work
  • Building a WebSocket Server
  • Rooms and Broadcasting
  • Authentication on WebSockets
  • Handling Disconnects and Reconnection

Patterns and Architecture

  • Pub/Sub
  • Presence
  • Scaling Real-Time

Putting It All Together

  • Choosing the Right Approach
  • Capstone: Live Task Board

SSE in Practice

The 6-connection limit

Browsers limit the number of simultaneous HTTP/1.1 connections per domain to 6. Each SSE connection uses one of these slots. If your app opens 6 SSE connections (to different boards, different feeds), no other HTTP requests can be made to the same domain.

Solutions:

HTTP/2: Multiplexes many streams over one connection. The 6-connection limit does not apply. Most modern servers and browsers support HTTP/2.

Single connection with filtering: Open one SSE connection and subscribe to topics within it. The server filters events based on the client’s subscriptions.

// Single connection with topics
const source = new EventSource("/events?boards=board-1,board-2");

Different subdomains: api.example.com for REST, stream.example.com for SSE. Each subdomain has its own connection limit.

For most apps, HTTP/2 solves this automatically. If you are on HTTP/1.1, use a single SSE connection.

Heartbeats

Some proxies and load balancers close idle connections after 60-120 seconds. If no events are sent during that time, the connection is killed. The browser reconnects, but the user experiences a delay.

Send heartbeat events to keep the connection alive:

// src/sse.ts — add heartbeat
const HEARTBEAT_INTERVAL = 15_000; // Every 15 seconds

setInterval(() => {
  for (const client of clients) {
    try {
      client.controller.enqueue(": heartbeat\n\n");
    } catch {
      const index = clients.indexOf(client);
      if (index !== -1) clients.splice(index, 1);
    }
  }
}, HEARTBEAT_INTERVAL);

The : heartbeat\n\n is an SSE comment (lines starting with : are ignored by the client). It keeps the connection open without triggering event handlers.

Connection counting

Track how many clients are connected for monitoring:

export function getConnectionCount(): number {
  return clients.length;
}

export function getConnectionCountByBoard(boardId: string): number {
  return clients.filter((c) => c.boardId === boardId).length;
}

// Expose as a monitoring endpoint
route.get("/admin/connections", {
  resolve: () => {
    return Response.json({
      total: getConnectionCount(),
      boards: Object.fromEntries(
        [...new Set(clients.map((c) => c.boardId))].map((id) => [
          id,
          getConnectionCountByBoard(id),
        ]),
      ),
    });
  },
});

Load balancing

Each SSE connection is bound to a specific server. If you have 3 servers behind a load balancer, a client connects to server 1. Events broadcast on server 2 are not seen by that client.

Solutions (covered in detail in the scaling lesson):

  • Sticky sessions: The load balancer routes the same client to the same server. Works but limits scalability.
  • Pub/sub backbone: All servers subscribe to a shared message bus (Redis). Events broadcast on any server are forwarded to all servers.

For a single-server deployment (which covers most apps), this is not an issue.

When SSE is enough

SSE is the right choice when:

  • The server pushes data and the client only listens (one-way)
  • You want automatic reconnection without writing reconnection logic
  • You need event types and resumption (Last-Event-ID)
  • Your app already uses HTTP and you do not want to add a new protocol

SSE is not enough when:

  • The client needs to send messages to the server frequently (use WebSockets)
  • You need binary data (SSE is text only)
  • You need sub-millisecond latency (WebSockets have slightly less overhead)

For a task board: SSE handles all the “push updates to viewers” use cases. WebSockets are needed for “typing indicators” and “real-time cursor position” — bidirectional features. The next section adds WebSockets.

Exercises

Exercise 1: Add the heartbeat. Connect with curl and observe the heartbeat comments arriving every 15 seconds.

Exercise 2: Add the connection count endpoint. Connect multiple clients and verify the count.

Exercise 3: Open 7 SSE connections from the same browser on HTTP/1.1. The 7th should stall (waiting for a connection slot). Close one, and the 7th connects.

Why do we send heartbeat comments instead of heartbeat events?

← Event Types and IDs How WebSockets Work →

© 2026 hectoday. All rights reserved.