web-realtime-sse

Server-Sent Events (SSE) Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "web-realtime-sse" with this command: npx skills add agents-inc/skills/agents-inc-skills-web-realtime-sse

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 required SSE headers: Content-Type: text/event-stream , Cache-Control: no-cache , Connection: keep-alive )

</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:

  • For code examples, see examples/

  • For decision frameworks and anti-patterns, see reference.md

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-ID header 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

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 - State tracking class const MAX_MANUAL_RETRIES = 5; const RETRY_DELAY_MS = 3000;

class SSEConnection { private eventSource: EventSource | null = null; private status: SSEStatus = "closed"; private manualRetryCount = 0; private onStatusChange?: (status: SSEStatus) => void; private onMessage?: (data: string, eventType: string) => void;

constructor( private url: string, options?: { onStatusChange?: (status: SSEStatus) => void; onMessage?: (data: string, eventType: string) => void; }, ) { this.onStatusChange = options?.onStatusChange; this.onMessage = options?.onMessage; }

connect(): void { if (this.eventSource) { this.disconnect(); }

this.setStatus("connecting");
this.eventSource = new EventSource(this.url);

this.eventSource.onopen = () => {
  this.setStatus("open");
  this.manualRetryCount = 0; // Reset on successful connection
};

this.eventSource.onmessage = (event: MessageEvent) => {
  this.onMessage?.(event.data, "message");
};

this.eventSource.onerror = () => {
  if (this.eventSource?.readyState === EventSource.CLOSED) {
    this.setStatus("closed");
    // EventSource won't auto-reconnect if server sent HTTP error
    this.attemptManualReconnect();
  } else {
    this.setStatus("error");
    // EventSource is auto-reconnecting
  }
};

}

private attemptManualReconnect(): void { if (this.manualRetryCount < MAX_MANUAL_RETRIES) { this.manualRetryCount++; console.log(Manual reconnect attempt ${this.manualRetryCount}); setTimeout(() => this.connect(), RETRY_DELAY_MS); } }

disconnect(): void { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; this.setStatus("closed"); } }

private setStatus(status: SSEStatus): void { this.status = status; this.onStatusChange?.(status); }

getStatus(): SSEStatus { return this.status; } }

export { SSEConnection };

Why good: Named constants for retry values, status tracking enables UI updates, manual retry for HTTP errors (EventSource only auto-retries network errors), cleanup resets state properly

Pattern 5: SSE Message Format

Understanding the SSE wire protocol is essential for both client and server implementation.

Message Structure

field: value\n field: value\n \n

Messages are separated by double newlines (\n\n ). Fields are separated by single newlines.

Field Types

Field Purpose Example

data:

Message content data: Hello World

event:

Custom event type event: notification

id:

Event ID for recovery id: 12345

retry:

Reconnection interval (ms) retry: 5000

:

Comment (keep-alive) : keep-alive

Example Messages

: This is a comment (keep-alive, ignored by client)

data: Simple text message

data: {"type": "update", "value": 42} id: msg-001

event: notification data: {"title": "New message"} id: msg-002

data: Multi-line data: message content data: spans multiple data fields id: msg-003

retry: 10000 data: Reconnect in 10 seconds if disconnected

Key behaviors:

  • Multiple data: lines are concatenated with newlines

  • id: persists across messages until changed

  • retry: is remembered for future reconnections

  • Comments (: ) are ignored but keep connection alive

Pattern 6: Discriminated Unions for Message Types

Use TypeScript discriminated unions for type-safe message handling.

// ✅ Good Example - Type-safe SSE message handling

// Server message types type SSEMessage = | { type: "notification"; title: string; body: string; priority: "low" | "high"; } | { type: "user-update"; userId: string; action: "joined" | "left" } | { type: "data-sync"; payload: unknown; timestamp: number } | { type: "heartbeat"; serverTime: number };

function parseSSEMessage(data: string): SSEMessage | null { try { return JSON.parse(data) as SSEMessage; } catch { console.error("Failed to parse SSE message:", data); return null; } }

function handleSSEMessage(message: SSEMessage): void { switch (message.type) { case "notification": showNotification(message.title, message.body, message.priority); break; case "user-update": updateUserPresence(message.userId, message.action); break; case "data-sync": syncData(message.payload, message.timestamp); break; case "heartbeat": updateServerTime(message.serverTime); break; default: // Exhaustiveness check const exhaustive: never = message; console.warn("Unknown message type:", exhaustive); } }

// Usage with EventSource eventSource.onmessage = (event: MessageEvent) => { const message = parseSSEMessage(event.data); if (message) { handleSSEMessage(message); } };

Why good: Discriminated union enables type narrowing, exhaustiveness check catches missing cases at compile time, separate parse and handle functions, error handling for malformed messages

Integration Guide

SSE is a transport mechanism. This skill covers the EventSource API and fetch streaming patterns only. Integration with specific frameworks or state management is handled by their respective skills.

Works with:

  • Your React framework via custom hooks (see examples/core.md)

  • Your state management solution for storing received data

  • Your authentication system for cookie-based auth or token management

Defers to:

  • Backend SSE server implementation (backend skills)

  • WebSocket patterns for bidirectional communication (websockets skill)

  • State management for storing and displaying received data (state management skills)

<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(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 required SSE headers: Content-Type: text/event-stream , Cache-Control: no-cache , Connection: keep-alive )

Failure to follow these rules will result in memory leaks, missed messages, and silent connection failures.

</critical_reminders>

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

infra-env-setup-env

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

infra-tooling-setup-tooling

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

meta-methodology-context-management

No summary provided by upstream source.

Repository SourceNeeds Review
Security

security-auth-security

No summary provided by upstream source.

Repository SourceNeeds Review