Server-Sent Events#

Real-time streaming from server to client using SSE. One-way communication from server to browser - ideal for live updates, progress bars, and AI streaming.

Info
SSE uses a single persistent HTTP connection. Unlike WebSocket, SSE is simpler, works over standard HTTP, and automatically reconnects.

Basic Usage#

typescript
app.get("/events", (ctx) => {
  return ctx.sse(async (sse) => {
    sse.sendData("Connected!");

    // Send a named event
    sse.sendEvent("update", { count: 42 });

    // Listen for client disconnect
    sse.onClose(() => {
      console.log("Client disconnected");
    });
  });
});

SSE API#

MethodDescription
sse.sendData(data)Send raw data as a string
sse.sendEvent(event, data)Send a named event with data (data is JSON-serialized)
sse.send(message)Send a full SSEMessage object with optional id, event, and data
sse.sendWithId(id, data)Send data with an explicit event ID (for resumption support)
sse.getLastEventId()Get the Last-Event-ID header value from the client
sse.close()Close the SSE connection
sse.onClose(callback)Register a close handler

Options#

OptionTypeDefaultDescription
sendInitialbooleantrueSend initial "connected" event
initialEventstring"connected"Custom event name for initial
retrynumberundefinedClient reconnection timeout (ms)
maxConnectionsnumber0 (unlimited)Max concurrent SSE connections. Returns 503 when exceeded.
onReconnect(lastEventId: string) => Promise<SSEMessage[]>undefinedCallback to replay missed events on reconnection
typescript
app.get("/events", (ctx) => {
  return ctx.sse(async (sse) => {
    // No auto "connected" event, retry every 5s
  }, {
    sendInitial: false,
    retry: 5000,
  });
});

Reconnection Support#

Use onReconnect to replay missed events when a client reconnects with a Last-Event-ID header:

typescript
// Store event history (in production, use Redis or database)
const eventHistory: SSEMessage[][] = [];

app.get("/events", (ctx) => {
  return ctx.sse(async (sse) => {
    // Send current state
    sse.sendEvent("state", { count: 42 });

    // Listen for updates
    emitter.on("update", (data) => {
      const message = { id: Date.now(), event: "update", data };
      sse.send(message);
      eventHistory.push([message]);
    });
  }, {
    onReconnect: async (lastEventId) => {
      // Return missed events since lastEventId
      return eventHistory
        .flat()
        .filter(msg => msg.id && msg.id > Number(lastEventId));
    },
  });
});

Connection Limits#

Limit concurrent connections to prevent resource exhaustion:

typescript
app.get("/events", (ctx) => {
  return ctx.sse(async (sse) => {
    // Handle events
  }, {
    maxConnections: 100, // Limit to 100 concurrent connections
  });
});

// Check active connections
console.log("Active connections:", SSE.activeConnections);

Broadcasting#

Use SSEBroadcaster to manage multiple connections and broadcast to all clients:

typescript
import { SSEBroadcaster } from "@buntok/core/sse";

const broadcaster = new SSEBroadcaster();

app.get("/events", (ctx) => {
  return ctx.sse(async (sse) => {
    broadcaster.add(sse);

    sse.onClose(() => {
      broadcaster.remove(sse);
    });

    // Keep connection alive
    await new Promise(() => {});
  });
});

// Broadcast to all clients
broadcaster.broadcast("update", { count: 42 });

// Broadcast with filter
broadcaster.broadcastWhere(
  (sse) => sse.isConnected,
  "notification",
  { message: "Hello!" }
);

// Check connected clients
console.log("Connected clients:", broadcaster.size);

Named Events#

Use named events so clients can listen for specific event types:

typescript
// Server
app.get("/notifications", (ctx) => {
  return ctx.sse(async (sse) => {
    emitter.on("user:created", (user) => {
      sse.sendEvent("user-created", user);
    });
    emitter.on("order:placed", (order) => {
      sse.sendEvent("order-placed", order);
    });
  });
});

// Client
const es = new EventSource("/notifications");
es.addEventListener("user-created", (e) => {
  const user = JSON.parse(e.data);
  console.log("New user:", user);
});
es.addEventListener("order-placed", (e) => {
  const order = JSON.parse(e.data);
  console.log("New order:", order);
});

AI Streaming#

Stream AI responses token-by-token:

typescript
import { streamAI } from "@buntok/core";

app.post("/chat", async (ctx) => {
  const { message } = await ctx.body();

  return ctx.sse(async (sse) => {
    await streamAI({
      messages: [{ role: "user", content: message }],
      onChunk: (chunk) => {
        sse.sendEvent("chunk", { content: chunk });
      },
      onDone: () => {
        sse.sendEvent("done", { message: "Complete" });
        sse.close();
      },
    });
  });
});

Live Updates#

typescript
// Push real-time data to clients
app.get("/live/metrics", (ctx) => {
  return ctx.sse(async (sse) => {
    const interval = setInterval(async () => {
      const metrics = await getMetrics();
      sse.sendEvent("metrics", metrics);
    }, 5000);

    sse.onClose(() => {
      clearInterval(interval);
    });
  });
});

SSE vs WebSocket#

FeatureSSEWebSocket
DirectionServer → ClientBidirectional
ProtocolHTTPws:// / wss://
Auto-reconnectYesNo (manual)
Binary dataNoYes
ComplexitySimpleModerate
Use caseLive feeds, notifications, AIChat, games, collaboration

Pluggable Infrastructure#

For production cluster deployments (multiple instances), replace in-memory PubSub and history with pluggable stores.

typescript
import {
  SSEBroadcaster,
  MemorySSEPubSub,     // default in-memory PubSub
  MemorySSEHistory,    // default in-memory ring buffer (1000 msg)
} from "@buntok/core/sse";

// Default (single instance)
const broadcaster = new SSEBroadcaster();

// Custom (e.g., Redis-backed for multi-instance)
const broadcaster = new SSEBroadcaster({
  pubSub: redisPubSub,      // implements SSEPubSub interface
  historyStore: redisHistory, // implements SSEHistoryStore interface
  channel: "events",        // pub/sub channel name
});
InterfaceMethodsDefault
SSEPubSubpublish, subscribe, unsubscribeMemorySSEPubSub
SSEHistoryStoreadd, getAfter, clearMemorySSEHistory (1000 msg ring buffer)
Info
Additional SSE methods: sse.sendComment(comment) for custom heartbeats, sse.offClose(callback) to unregister close handlers, and SSE.closeAll() to gracefully close all connections.

Next Steps#