Lesson 2.2: Server-Sent Events
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Explain what Server-Sent Events (SSE) are and how they work
- Understand the SSE protocol and message format
- Compare SSE with other real-time technologies
- Parse and handle SSE streams in TypeScript
What Are Server-Sent Events?
Server-Sent Events (SSE) is a web standard that allows servers to push data to clients over HTTP. Unlike traditional request-response patterns where the client asks and the server answers once, SSE keeps the connection open and sends multiple messages over time.
Think of it like a radio broadcast: you tune in once, and the station keeps sending you content until you disconnect.
How SSE Works
The SSE flow is simple:
- Client opens an HTTP connection to the server
- Server keeps the connection open
- Server sends messages as events occur
- Client receives and processes each message
- Connection stays open until client or server closes it
Client Server
| |
|------- GET /stream ---->|
| |
|<------ data: Hello -----|
|<------ data: World -----|
|<------ data: [DONE] ----|
| |
SSE Message Format
SSE messages follow a specific text format. Each message consists of fields separated by newlines:
data: This is a message
data: This is another message
event: custom-event
data: {"key": "value"}
Key fields in SSE:
- data: The actual message content (required)
- event: Optional event type name
- id: Optional message identifier
- retry: Suggested reconnection time in milliseconds
Messages are separated by blank lines (two newlines).
SSE in AI APIs
Both OpenAI and Anthropic use SSE to stream responses. When you enable streaming, the API sends chunks like this:
data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"Hello"}}]}
data: {"id":"chatcmpl-123","choices":[{"delta":{"content":" world"}}]}
data: [DONE]
Each data: line contains a JSON object with the next piece of the response. The [DONE] marker signals the stream is complete.
Parsing SSE Manually
Understanding the raw format helps with debugging. Here is a basic SSE parser:
function parseSSE(rawData: string): string[] {
const messages: string[] = [];
const lines = rawData.split("
");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6); // Remove "data: " prefix
if (data !== "[DONE]") {
messages.push(data);
}
}
}
return messages;
}
// Example usage
const raw = `data: {"content":"Hello"}
data: {"content":" World"}
data: [DONE]`;
const messages = parseSSE(raw);
// ["{"content":"Hello"}", "{"content":" World"}"]
In practice, you will use the SDK which handles parsing automatically.
SSE vs Other Technologies
| Technology | Direction | Connection | Use Case |
|---|---|---|---|
| SSE | Server to Client | Long-lived | Streaming AI responses |
| WebSockets | Bidirectional | Long-lived | Chat apps, games |
| HTTP Polling | Client to Server | Repeated | Legacy systems |
| Long Polling | Both | Repeated | Fallback option |
SSE is ideal for AI streaming because:
- Responses flow one direction (server to client)
- Built on standard HTTP (works everywhere)
- Automatic reconnection support
- Simpler than WebSockets
Handling SSE in Node.js
When working with SSE streams in Node.js, you typically use async iterators:
async function processStream(response: Response): Promise<string> {
const reader = response.body?.getReader();
if (!reader) throw new Error("No response body");
const decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("
");
for (const line of lines) {
if (line.startsWith("data: ") && line !== "data: [DONE]") {
const json = JSON.parse(line.slice(6));
const content = json.choices?.[0]?.delta?.content ?? "";
result += content;
process.stdout.write(content);
}
}
}
return result;
}
The SDK abstracts this complexity, but understanding the underlying mechanism helps with debugging.
Error Handling in SSE
SSE connections can fail for various reasons:
async function streamWithRetry(url: string, maxRetries: number = 3): Promise<void> {
let attempts = 0;
while (attempts < maxRetries) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
await processStream(response);
return; // Success
} catch (error) {
attempts++;
console.error(`Attempt ${attempts} failed:`, error);
if (attempts < maxRetries) {
const delay = Math.pow(2, attempts) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts`);
}
Common SSE errors:
- Network disconnection
- Server timeout
- Rate limiting (429 status)
- Invalid response format
Browser vs Node.js
SSE is a web standard with native browser support via EventSource:
// Browser-only
const source = new EventSource('/api/stream');
source.onmessage = (event) => {
console.log('Received:', event.data);
};
source.onerror = (error) => {
console.error('SSE error:', error);
};
In Node.js, there is no native EventSource, so we use:
- The SDKs native streaming support
- Fetch API with manual parsing
- Third-party libraries like
eventsource
Key Takeaways
- SSE is a web standard for server-to-client streaming over HTTP
- Messages use a simple text format with
data:prefixes - AI APIs use SSE to stream token-by-token responses
- The SDK handles parsing but understanding the format helps debugging
- SSE is simpler than WebSockets for one-way streaming
Resources
| Resource | Type | Level |
|---|---|---|
| MDN Server-Sent Events | Documentation | Beginner |
| SSE Specification | Specification | Advanced |
| OpenAI Streaming Format | Documentation | Beginner |
Next Lesson
Now that you understand how SSE works, you are ready to implement streaming with real AI APIs. In the next lesson, you will build streaming responses with OpenAI.