Imagine you're building a railway line between two cities.
One engineer suggests building a complex, double-decker magnetic-levitation track with computerized bi-directional switching bays and emergency passing lanes. It costs millions, requires twenty maintenance technicians at every junction, and breaks whenever there is a minor power fluctuation.
Another engineer looks at the requirements and says: "All we need to do is send coal from the mine in City A down to the furnace in City B. Why don't we just use a simple downhill conveyor belt?"
In modern web development, WebSockets are the over-engineered maglev train, and Server-Sent Events (SSE) are the bulletproof conveyor belt.
When developers build real-time AI features—streaming token completions, showing background ingestion progress, or notifying users when their knowledge card is synthesized—their knee-jerk reaction is almost always: "Let's set up WebSockets."
When we built Vault, we evaluated both architectures under heavy real-world load.
Here is why choosing Server-Sent Events over WebSockets was one of the cleanest, most resilient architectural decisions in our entire product.
1. The Fundamental Asymmetry of AI Products
To pick the right network protocol, you must look at the physics of how users interact with AI.
Is an AI product like a multiplayer video game or a live collaborative canvas (Figma) where hundreds of clients are sending rapid bi-directional messages back and forth every millisecond?
No.
AI interactions are almost completely asymmetrical:
- The client sends a single, compact HTTP request (e.g., a prompt or a URL).
- The server processes the request over 2 to 10 seconds.
- The server continuously pushes incremental chunks back to the client (status updates, tokens, generated knowledge cards).
Client ───────[ Single POST Request (1KB) ]───────> Server
Client <───────[ Stream: Resolving video... ]────── Server
Client <───────[ Stream: Transcribing audio... ]── Server
Client <───────[ Stream: Token 1, 2, 3... ]─────── Server
Client <───────[ Stream: Ingestion Complete ]───── Server
The client does not need to talk back during the generation stream. It is a strictly unidirectional downstream feed.
2. The WebSocket Trap in Modern Serverless Infrastructure
WebSockets are full-duplex, persistent TCP connections. On paper, that sounds great. In production across modern edge and serverless platforms (Vercel, AWS Lambda, Cloudflare), WebSockets introduce massive operational friction:
The WebSocket Tax:
- Stateful Connections vs Stateless Compute: Serverless functions have execution timeouts. A persistent WebSocket connection requires dedicated stateful servers (EC2, Fly.io, or specialized gateway services like Pusher / AWS API Gateway WebSockets).
- Load Balancer Nightmare: Standard load balancers must maintain sticky sessions and handle connection draining on every zero-downtime deployment.
- Heartbeats & Reconnection Storms: You have to write custom ping/pong heartbeat logic, handle zombie sockets, and manage exponential backoff on client reconnects.
- Firewalls & Corporate Proxies: Many corporate firewalls and strict network proxies silently drop or throttle non-standard WebSocket upgrade requests (
Upgrade: websocket).
graph LR
subgraph WebSockets
C1[Client] <-->|Stateful Persistent TCP| LB1[Load Balancer / Sticky Sockets]
LB1 <--> S1[Dedicated Always-On Server]
end
subgraph SSE
C2[Client] -->|Standard HTTP POST| S2[Serverless Next.js API Route]
S2 -->|Standard HTTP/2 Stream| C2
end
3. The Elegance of Server-Sent Events (SSE)
Server-Sent Events are built right on top of standard HTTP. When running over HTTP/2, SSE gives you all the benefits of real-time streaming with none of the stateful baggage.
Why SSE Wins for AI Pipelines:
- Zero Special Infrastructure: An SSE endpoint is just a standard Next.js API route / HTTP endpoint returning
Content-Type: text/event-stream. It runs natively on Vercel, Node, Bun, or Cloudflare Workers. - Native HTTP/2 Multiplexing: Over HTTP/2, multiple SSE streams share a single underlying TCP connection alongside all other web requests. No socket limits.
- Built-in Auto-Reconnection: The browser's native
EventSourceautomatically handles reconnection and tracks message IDs out of the box with zero custom client libraries. - Simple Observability & Debugging: You can inspect an SSE stream in the Chrome DevTools Network tab exactly like any other HTTP request. You can test it with
curl -N https://api.vault.com/api/ingest/stream.
4. How We Implemented SSE in Vault
In Vault, when a user enters a Reel URL or waits for an ingestion job to complete, the frontend establishes an SSE stream to observe real-time progress:
The Server Implementation (Next.js App Router):
// app/api/reels/stream/route.ts
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const reelId = searchParams.get("id");
const responseStream = new TransformStream();
const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder();
const sendEvent = async (event: string, data: any) => {
await writer.write(
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
);
};
// Run pipeline asynchronously and stream progress
(async () => {
try {
await sendEvent("status", { stage: "resolving", message: "Fetching media..." });
// ... worker media extraction
await sendEvent("status", { stage: "transcribing", message: "Extracting spoken signals..." });
// ... transcription
await sendEvent("status", { stage: "synthesizing", message: "Distilling core knowledge..." });
// ... insight extraction
await sendEvent("completed", { reelId, status: "ready" });
} catch (err: any) {
await sendEvent("error", { message: err.message });
} finally {
await writer.close();
}
})();
return new Response(responseStream.readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
The Frontend Hook:
// hooks/useReelStream.ts
export function useReelStream(reelId: string) {
const [status, setStatus] = useState<string>("idle");
useEffect(() => {
const eventSource = new EventSource(`/api/reels/stream?id=${reelId}`);
eventSource.addEventListener("status", (e) => {
const data = JSON.parse(e.data);
setStatus(data.message);
});
eventSource.addEventListener("completed", () => {
eventSource.close();
// Optimistically trigger UI reload
});
eventSource.onerror = () => {
eventSource.close();
};
return () => eventSource.close();
}, [reelId]);
return { status };
}
The Verdict: Architecture is About Restraint
Great engineering is not about using the most complex protocol available. It is about using the simplest protocol that completely solves the problem with zero accidental complexity.
For multiplayer gaming or live audio chat, use WebSockets or WebRTC.
For AI token streaming, status feeds, and knowledge ingestion pipelines: Server-Sent Events are unbeatable in elegance, cost, and reliability.