Server-Sent Events (SSE) Patterns
Quick Guide: Use SSE for unidirectional server-to-client real-time updates over HTTP. Use the native EventSource API for automatic reconnection and message parsing. Use fetch streaming when you need custom headers or POST requests.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST use named constants for ALL timing values - reconnect intervals, keep-alive periods, timeouts)
(You MUST implement proper cleanup by calling eventSource.close() when connections are no longer needed)
(You MUST use event IDs (id: field) to enable message recovery on reconnection)
(You MUST handle the onerror event and check readyState to distinguish reconnection from permanent failure)
(You MUST set Content-Type: text/event-stream and Cache-Control: no-cache on SSE responses — do NOT set Connection: keep-alive on HTTP/2+)
</critical_requirements>
Auto-detection: SSE, Server-Sent Events, EventSource, text/event-stream, onmessage, server push, one-way streaming, real-time updates
When to use:
- Server-to-client real-time updates (notifications, feeds, dashboards)
- LLM/AI response streaming (token-by-token output)
- Live data feeds (stock prices, sports scores, news)
- Server push notifications without client responses needed
- Long-polling replacement with better browser support
Key patterns covered:
- EventSource API connection lifecycle
- Custom event types with addEventListener
- Fetch-based streaming for custom headers/POST
- SSE message parsing (data, event, id, retry fields)
- Reconnection with Last-Event-ID recovery
- Keep-alive comments to prevent proxy timeouts
- Custom React hooks (useEventSource, useSSE)
When NOT to use:
- Bidirectional communication needed (use WebSocket)
- Binary data transmission required (use WebSocket)
- Client needs to send frequent messages (use WebSocket)
- Sub-millisecond latency required (use WebSocket)
Detailed Resources:
- examples/core.md - React hooks (useEventSource, useSSE), shared context, conditional connection
- examples/fetch-streaming.md - Fetch-based SSE, message parser, auth, POST streaming, LLM pattern
- examples/reconnection.md - Last-Event-ID recovery, exponential backoff, health checks, visibility-aware
- reference.md - Decision frameworks, anti-patterns, message format reference
<philosophy>
Philosophy
Server-Sent Events (SSE) provide a simple, HTTP-based protocol for servers to push real-time updates to clients. Unlike WebSockets, SSE is unidirectional (server to client only), built on standard HTTP, and includes automatic reconnection.
Why SSE exists:
-
Simplicity: Standard HTTP protocol - works through firewalls, proxies, and load balancers without special configuration.
-
Built-in Reconnection: The EventSource API automatically reconnects when connections drop, with configurable retry intervals.
-
Message Recovery: The
Last-Event-IDheader enables servers to replay missed messages after reconnection. -
Text-Based Protocol: Human-readable format makes debugging straightforward.
Connection Lifecycle:
CONNECTING (0) → OPEN (1) → messages... → CLOSED (2)
↓ ↓
(error) ← auto-reconnect ← (connection lost)
When to Choose SSE over WebSocket:
- Server sends updates, client only listens
- Working with HTTP/2 (multiplexing multiple SSE streams)
- Need automatic reconnection without custom logic
- Proxies/firewalls block WebSocket but allow HTTP
- Building LLM streaming interfaces
<patterns>
Core Patterns
Pattern 1: Basic EventSource Connection
The native EventSource API provides automatic connection management, message parsing, and reconnection.
Constants
const SSE_URL = "/api/events";
Implementation
// ✅ Good Example - Complete lifecycle handling
const SSE_URL = "/api/events";
const eventSource = new EventSource(SSE_URL);
eventSource.onopen = () => {
console.log("SSE connection opened");
// Connection is ready - server can now push events
};
eventSource.onmessage = (event: MessageEvent) => {
console.log("Received:", event.data);
console.log("Event ID:", event.lastEventId);
};
eventSource.onerror = (error: Event) => {
console.error("SSE error:", error);
// Check connection state to determine action
if (eventSource.readyState === EventSource.CLOSED) {
console.log("Connection closed permanently");
} else if (eventSource.readyState === EventSource.CONNECTING) {
console.log("Reconnecting...");
}
};
// Cleanup when done
// eventSource.close();
Why good: All three lifecycle events handled, readyState check distinguishes reconnection from permanent failure, named constant for URL, cleanup shown
// ❌ Bad Example - Missing error handling and cleanup
const eventSource = new EventSource("/api/events");
eventSource.onmessage = (event) => {
console.log(event.data);
};
// No onerror handler - connection failures are silent
// No cleanup - connection stays open forever
Why bad: Missing onerror means failures are silent, missing cleanup causes memory leaks and zombie connections, hardcoded URL string
Pattern 2: Custom Event Types
SSE supports named events via the event: field. Use addEventListener to handle specific event types.
// ✅ Good Example - Multiple event type handling
const SSE_URL = "/api/notifications";
const eventSource = new EventSource(SSE_URL);
// Default message event (no event: field in server response)
eventSource.onmessage = (event: MessageEvent) => {
console.log("Generic message:", event.data);
};
// Named custom events
eventSource.addEventListener("notification", (event: MessageEvent) => {
const notification = JSON.parse(event.data);
showNotification(notification.title, notification.body);
});
eventSource.addEventListener("user-joined", (event: MessageEvent) => {
const user = JSON.parse(event.data);
updateUserList(user);
});
eventSource.addEventListener("heartbeat", (event: MessageEvent) => {
// Keep-alive event - connection is healthy
console.log("Heartbeat received at:", event.data);
});
Why good: Separate handlers for different event types, typed MessageEvent parameters, JSON parsing for structured data, heartbeat handling for connection health
When to use: When server sends multiple types of events with different handling requirements.
Pattern 3: Credentials and Cross-Origin
For cross-origin requests or when cookies are required, configure withCredentials.
// ✅ Good Example - Cross-origin with credentials
const SSE_URL = "https://api.example.com/events";
const eventSource = new EventSource(SSE_URL, {
withCredentials: true, // Include cookies for cross-origin
});
eventSource.onopen = () => {
console.log("Connected with credentials");
};
eventSource.onerror = (error) => {
// CORS errors will trigger onerror
console.error("Connection error - check CORS configuration");
};
Why good: withCredentials enables cookie-based authentication, CORS error handling noted
When to use: Cross-origin SSE connections that require authentication cookies.
Pattern 4: Connection State Management
Track connection state for UI feedback and smart reconnection decisions.
Constants
type SSEStatus = "connecting" | "open" | "closed" | "error";
const READY_STATE_MAP: Record<number, SSEStatus> = {
[EventSource.CONNECTING]: "connecting",
[EventSource.OPEN]: "open",
[EventSource.CLOSED]: "closed",
};
Implementation
// ✅ Good Example - Stat