Command Palette

Search for a command to run...

All postsEngineering

Building Real-Time Apps with WebSockets & Next.js

SB
Sachin Babu
Apr 12, 20257 min read

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.

All postsEnd of article

Keep Reading

All Posts →