Real-time features -- live notifications, chat, dashboards that update without refresh -- require a way to get data from server to client without the client explicitly requesting it. Three approaches exist: polling, Server-Sent Events, and WebSockets. Each is the right choice in different situations.
Polling: The Simplest Approach
Polling means the client sends a request to the server at a fixed interval. Every 5 seconds, every 30 seconds, every minute -- whatever the use case requires.
useEffect(() => {
const interval = setInterval(async () => {
const data = await fetch("/api/status").then(r => r.json());
setStatus(data);
}, 5000);
return () => clearInterval(interval);
}, []);
When to use polling: Infrequent updates where a few seconds of latency is acceptable. Deployment status checks, background job progress, simple notification counts. Any situation where the update interval can be 30+ seconds.
Advantages: Trivially simple to implement. Works everywhere. Compatible with serverless functions. No persistent connection to maintain. Easy to debug (it is just HTTP requests).
Disadvantages: Sends requests even when nothing has changed (wasteful). Higher latency proportional to the polling interval. Scales poorly as user count increases -- each user generates N requests per minute.
Long polling is a variation: the server holds the request open until data is available, then responds. The client immediately sends another request. This reduces wasted requests but is complex to implement correctly and has connection timeout issues.
Server-Sent Events: One-Way Push
SSE is an HTTP-based protocol where the server sends a stream of events to the client over a persistent connection. The client subscribes once; the server pushes whenever it has something to send. The connection is one-directional: server to client only.
// Client
useEffect(() => {
const source = new EventSource("/api/notifications/stream");
source.onmessage = (e) => {
const notification = JSON.parse(e.data);
addNotification(notification);
};
return () => source.close();
}, []);
// Server (Next.js Route Handler)
export async function GET() {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// Send events as they occur
const send = (data: object) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
`));
};
// Subscribe to events...
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
When to use SSE: Live feeds, notifications, progress updates, any stream where the server pushes but the client does not send updates back. AI response streaming (ChatGPT-style streaming output) uses SSE. Dashboard metrics that update in real-time. Deployment logs streamed to the browser.
Advantages: Simple protocol, built on HTTP, works through proxies and firewalls. Native browser support (EventSource API). Automatic reconnection built into the browser. Lower overhead than WebSockets for one-directional data.
Disadvantages: One-directional only. The client cannot send data on the same connection. Maximum of 6 concurrent SSE connections per domain in HTTP/1.1 (HTTP/2 removes this limit).