// LOADING
// LOADING
// LOADING_ARTICLE
Adding live data to a web application usually leads to the question: WebSockets or Server-Sent Events? Both push data from server to client without polling, but their protocols, capabilities, and operational characteristics differ significantly. Choosing the wrong one costs you complexity you don't need or capability you have to bolt on later.
This post is a decision guide, not a sales pitch for either technology. The right choice depends on your traffic pattern, infrastructure, and what "real-time" actually means in your application.
WebSocket is a full-duplex, persistent TCP connection negotiated via an HTTP upgrade handshake:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
After the handshake, both sides can send frames at any time. The connection stays open until either side closes it.
SSE is a one-way channel: server to client only. It rides on a plain HTTP response with Content-Type: text/event-stream. The browser's built-in EventSource API handles reconnection automatically.
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"type":"update","value":42}\n\n
data: {"type":"update","value":43}\n\n
Each event is a block of data:, event:, id:, and retry: lines, terminated by a blank line.
| Dimension | WebSockets | Server-Sent Events | |---|---|---| | Direction | Full-duplex | Server → client only | | Protocol | WS / WSS | HTTP / HTTPS | | Browser reconnect | Manual | Automatic (built in) | | HTTP/2 multiplexing | No | Yes | | Proxy / firewall friendliness | Sometimes problematic | Generally fine | | Binary data | Native | Base64 only | | Browser support | Universal | Universal (IE excluded) |
The HTTP/2 multiplexing point is meaningful: with SSE you can have hundreds of open event streams over a single TCP connection per client. WebSocket connections each require their own TCP connection.
SSE is the right default for many real-time features because of its simplicity:
SSE in a Next.js Route Handler:
typescript// app/api/stream/route.ts import { NextRequest } from 'next/server'; export async function GET(req: NextRequest) { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const send = (data: unknown) => { controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); }; // Example: push an update every second for 10 seconds for (let i = 0; i < 10; i++) { send({ count: i, ts: Date.now() }); await new Promise((r) => setTimeout(r, 1000)); } controller.close(); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }); }
Client-side consumption:
typescriptconst source = new EventSource('/api/stream'); source.onmessage = (e) => { const data = JSON.parse(e.data); console.log(data); }; source.onerror = () => source.close();
WebSockets are the right choice when the client needs to send messages back through the same persistent connection:
A minimal WebSocket server with the ws library:
typescriptimport { WebSocketServer, WebSocket } from 'ws'; import { createServer } from 'http'; const httpServer = createServer(); const wss = new WebSocketServer({ server: httpServer }); wss.on('connection', (ws: WebSocket) => { ws.on('message', (raw) => { const msg = JSON.parse(raw.toString()); // Broadcast to all connected clients wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ echo: msg })); } }); }); ws.on('close', () => console.log('client disconnected')); }); httpServer.listen(3001);
This is where most tutorials stop short.
SSE scales more naturally on stateless serverless platforms. Each request is an independent streaming response. Load balancers handle them like any long-running HTTP request. The limitation: Vercel Edge Functions and similar platforms cap response streaming duration, so SSE works well for short-lived streams and less well for indefinite connections.
WebSockets require sticky sessions (or a message broker). When a client connects to server A and another client is on server B, they cannot exchange messages unless both servers share state through something like Redis Pub/Sub or a managed service (Ably, Pusher, Liveblocks, Soketi). This is the single biggest operational cost of WebSockets at scale.
For most non-trivial WebSocket deployments, you will end up running a dedicated WebSocket service separate from your Next.js application — exactly as described in background jobs and queues in Node.js where the principle of separating concerns by process applies equally here.
Start with SSE. If at any point you find yourself needing the client to send structured messages back through the same persistent connection (not REST calls, but truly bidirectional stream semantics), switch to WebSockets. The vast majority of "real-time" features — notifications, live counts, streaming data, LLM tokens — are push-only and SSE is sufficient.
For authentication on either transport, the patterns in How Modern Authentication Works apply: validate a token when the connection is established and re-check on reconnect.
SSE is simpler, HTTP-native, and right for most real-time needs. WebSockets add true bidirectionality at the cost of stateful connections and operational complexity. Pick SSE as your default, introduce WebSockets only when bidirectional streaming is genuinely required, and plan your scaling strategy before you deploy.
Browse the projects page to see real-time features built with these patterns, or visit interview-prep for common real-time system design questions.