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).
WebSockets: Bidirectional, Low Latency
WebSockets provide a persistent, bidirectional connection. After an initial HTTP handshake, the connection upgrades to a WebSocket protocol. Both client and server can send messages at any time with very low overhead.
// Client
const socket = new WebSocket("wss://api.example.com/ws");
socket.onmessage = (e) => {
const message = JSON.parse(e.data);
handleMessage(message);
};
socket.send(JSON.stringify({ type: "join", room: "general" }));
When to use WebSockets: Chat applications (both sending and receiving). Multiplayer games. Collaborative document editing. Any feature where the client sends frequent messages to the server and receives responses. Live cursors, live presence indicators.
Advantages: True bidirectional communication. Very low per-message overhead (just a few bytes of framing). Lowest latency of the three approaches. Designed for high-frequency messaging.
Disadvantages: More complex to implement correctly. Long-lived connections require careful connection management. Stateful -- problematic with serverless functions and load balancers without sticky sessions.
The Next.js Consideration
Serverless functions (Vercel, AWS Lambda, Netlify Functions) do not support long-lived connections natively. A WebSocket connection needs to stay open for minutes or hours. A serverless function times out after seconds.
This means for WebSockets in Next.js deployed serverlessly, you need a separate WebSocket server (a Node.js process that stays running) or a hosted WebSocket service.
Hosted options: Ably, Pusher, Soketi (self-hosted Pusher-compatible), Liveblocks (collaborative-specific). These handle the connection management and let your Next.js app send messages via HTTP to the WebSocket service, which delivers them to connected clients.
SSE is more serverless-friendly because each connection is a streaming HTTP response. Vercel supports streaming responses. However, long SSE connections still have duration limits on serverless platforms.
For Next.js apps that need bidirectional real-time, the cleanest architecture is: Next.js handles the rest of the app, a separate Node.js server (the same one that runs the custom server for Socket.IO, for example) handles WebSocket connections.
Socket.IO: When It Is the Right Choice
Socket.IO is a library that wraps WebSockets with additional features: automatic fallback to long polling if WebSockets are not available, automatic reconnection, rooms and namespaces, and acknowledgments.
Use Socket.IO when: you need the room/namespace abstraction for a chat or collaboration feature, you need guaranteed delivery with acknowledgments, or you need to support environments where WebSockets might be blocked by corporate firewalls (Socket.IO falls back to polling).
Avoid Socket.IO when: you are building a simple real-time feature that SSE or native WebSockets handles cleanly, you are deploying to serverless (Socket.IO needs a persistent Node.js server), or the extra library weight is not justified by the feature complexity.
Summary: Which to Use
Polling: infrequent updates (30+ second intervals), serverless deployment, simplicity is a priority.
SSE: server-to-client push with moderate frequency, streaming data (AI responses, logs), no need for client-to-server messages on the same connection.
WebSockets: bidirectional real-time communication, chat, multiplayer, collaboration, high-frequency messaging.
Keep Reading
- Next.js API Routes Best Practices: Patterns for Production APIs -- building the server side of real-time features
- Next.js Performance Optimization: The Practical Guide for Production Apps -- real-time features and their performance implications
- Database Connection Pooling in Next.js: Solving the Serverless Problem -- connection management patterns relevant to WebSockets
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace -- chat, projects, time tracking, AI meeting summaries, and invoicing -- in one tool. Try it free.