Real-time communication is no longer a luxury — it's an expectation. Whether you're building a collaborative editor, a live dashboard, or a chat app, polling HTTP endpoints every few seconds is a dead end. WebSockets give you a persistent, bidirectional channel with sub-10ms latency. This post walks through wiring them into a Next.js app the right way.
Why WebSockets Over Polling
Polling is simple but wasteful. At scale, even lightweight polling floods your infrastructure. Long-polling is a half-measure. Server-Sent Events (SSE) are fine for one-way streams but collapse the moment you need the client to send messages back.
WebSockets are the real answer: a single TCP connection stays open for the lifetime of the session. No repeated handshakes, no bloated HTTP headers on every message.
Setting Up the Server
Next.js isn't a WebSocket server. You need a separate Node process — ws is lightweight, or you can reach for Socket.io if you need room support and automatic reconnects.
// server/ws.ts
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 3001 });
wss.on("connection", (socket) => {
socket.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
// broadcast to all clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg));
}
});
});
});The Client Hook
Wrap the native WebSocket API in a custom hook so React can manage the lifecycle cleanly.
import { useEffect, useRef, useCallback } from "react";
export function useWebSocket(url: string, onMessage: (data: unknown) => void) {
const ws = useRef<WebSocket | null>(null);
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onmessage = (e) => onMessage(JSON.parse(e.data));
return () => ws.current?.close();
}, [url]);
const send = useCallback((data: unknown) => {
ws.current?.send(JSON.stringify(data));
}, []);
return { send };
}Production Concerns
Reconnection logic — networks drop. Implement exponential backoff on close events. Libraries like reconnecting-websocket do this for free.
Authentication — don't pass tokens in query params. Use the first message after connection to send a signed token, verify it server-side, and only then start streaming data.
Horizontal scaling — multiple Node instances each hold their own connections. Use Redis Pub/Sub as the message bus so any instance can broadcast to all clients regardless of which server holds the socket.
Next.js App Router + WebSockets — App Router server components can't hold stateful connections. Keep your WebSocket server completely separate, and use a client component to manage the hook. This is actually cleaner — clear boundary between UI and real-time logic.
The combination of a standalone WebSocket server with Redis-backed pub/sub and a clean React hook gives you a pattern that scales from a side project to millions of concurrent users without fundamental rearchitecting.