| name | websockets |
| description | - Building real-time bidirectional features (chat, live dashboards, notifications, multiplayer) |
WebSockets and Socket.io
When to activate
- Building real-time bidirectional features (chat, live dashboards, notifications, multiplayer)
- Setting up Socket.io server with authentication middleware
- Managing rooms and namespaces for channel-based messaging
- Scaling Socket.io horizontally with a Redis adapter
- Implementing client-side reconnection logic
- Transmitting binary data over WebSocket
When NOT to use
- One-directional streaming from server to client only — use Server-Sent Events (SSE) instead
- Simple request/response patterns where HTTP polling is acceptable and latency is not critical
- When the transport layer is already handled by a managed service (Pusher, Ably, etc.) and you only need the client SDK
Instructions
Socket.io Server Setup with Auth Middleware
import { Server, Socket } from "socket.io";
import { createServer } from "http";
import { verify } from "jsonwebtoken";
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: process.env.CLIENT_URL ?? "http://localhost:3000",
methods: ["GET", "POST"],
credentials: true,
},
pingTimeout: 20000,
pingInterval: 25000,
transports: ["websocket", "polling"],
});
io.use(async (socket, next) => {
try {
const token =
socket.handshake.auth.token ??
socket.handshake.headers.authorization?.replace("Bearer ", "");
if (!token) return next(new Error("Authentication required"));
const payload = verify(token, process.env.JWT_SECRET!) as { userId: string; role: string };
socket.data.userId = payload.userId;
socket.data.role = payload.role;
next();
} catch {
next(new Error("Invalid token"));
}
});
io.on("connection", (socket: Socket) => {
const { userId, role } = socket.data;
console.log(`[socket] connected: ${socket.id} user=${userId}`);
socket.on("disconnect", (reason) => {
console.log(`[socket] disconnected: ${socket.id} reason=${reason}`);
});
registerChatHandlers(io, socket);
registerPresenceHandlers(io, socket);
});
export { io, httpServer };
Room Management — Join, Leave, Broadcast
Rooms are server-side namespaces. A socket can be in multiple rooms. Rooms do not persist — they are created on first join and destroyed when empty.
export function registerChatHandlers(io: Server, socket: Socket) {
socket.on("chat:join", async (roomId: string) => {
await socket.join(roomId);
socket.to(roomId).emit("chat:user_joined", {
userId: socket.data.userId,
roomId,
});
socket.emit("chat:joined", { roomId, success: true });
});
socket.on("chat:message", async (payload: { roomId: string; text: string }) => {
const { roomId, text } = payload;
if (!socket.rooms.has(roomId)) {
socket.emit("chat:error", { message: "Not in this room" });
return;
}
const message = ({ : socket.., roomId, text });
io.(roomId).(, message);
});
socket.(, {
socket.(roomId);
socket.(roomId).(, { : socket.. });
});
socket.(, {
( room socket.) {
(room !== socket.) {
socket.(room).(, { : socket.. });
}
}
});
}
Broadcast targets:
io.emit("event", data);
io.to("room-id").emit("event", data);
socket.to("room-id").emit("event", data);
socket.broadcast.emit("event", data);
socket.emit("event", data);
io.except("room-id").emit("event", data);
Redis Adapter for Horizontal Scaling
Without Redis, events emitted on one server process are not received by sockets connected to another process.
npm install @socket.io/redis-adapter ioredis
import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "ioredis";
const pubClient = createClient({ host: process.env.REDIS_HOST, port: 6379 });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
With the Redis adapter, Socket.io serializes events through Redis pub/sub. All server instances share room membership. For room state lookups across instances use io.in("room").fetchSockets() (async).
Additional considerations:
- Use a sticky session load balancer (e.g., NGINX
ip_hash, AWS ALB stickiness) when polling is enabled — polling requires all requests from a client to hit the same server during the handshake phase
- If using WebSocket transport only (
transports: ["websocket"]), sticky sessions are not required
Client Reconnection with Exponential Backoff
Socket.io client handles reconnection automatically. Configure it explicitly:
import { io, Socket } from "socket.io-client";
let socket: Socket;
export function createSocket(token: string): Socket {
socket = io(process.env.NEXT_PUBLIC_API_URL!, {
auth: { token },
transports: ["websocket"],
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 30000,
randomizationFactor: 0.5,
timeout: 10000,
});
socket.on("connect", () => {
console.log("[socket] connected:", socket.id);
rejoinRooms();
});
socket.on("connect_error", (err) => {
if (err.message === ) {
socket.();
();
}
.(, err.);
});
socket.(, {
.(, reason);
(reason === ) {
socket.();
}
});
socket;
}
Re-join rooms after reconnect since room membership is stored in memory per server process:
function rejoinRooms() {
const rooms = getRoomStateFromLocalStorage();
rooms.forEach((roomId) => socket.emit("chat:join", roomId));
}
Event Naming Conventions
Consistent event naming prevents conflicts and aids debugging. Use namespace:action format:
chat:message
chat:join
chat:leave
chat:typing_start
chat:typing_stop
presence:online
presence:offline
notification:new
room:created
room:deleted
error # reserved by Socket.io — use sparingly
Avoid generic names like data, update, event. Always use lowercase and underscores within the action segment.
Define event types in a shared module:
export interface ServerToClientEvents {
"chat:message": (msg: ChatMessage) => void;
"chat:user_joined": (info: { userId: string; roomId: string }) => void;
"presence:online": (userId: string) => void;
"notification:new": (notif: Notification) => void;
}
export interface ClientToServerEvents {
"chat:join": (roomId: string, callback: (success: boolean) => void) => void;
"chat:message": (payload: { roomId: string; text: string }) => void;
"chat:leave": (roomId: string) => void;
}
const io = <, >(httpServer);
: <, > = ();
Binary Data over WebSocket
Socket.io supports sending binary data (Buffer, ArrayBuffer, Blob, TypedArray) natively:
socket.on("file:request", async (fileId: string) => {
const buffer: Buffer = await readFile(fileId);
socket.emit("file:data", { id: fileId, buffer });
});
socket.on("file:data", ({ id, buffer }: { id: string; buffer: ArrayBuffer }) => {
const blob = new Blob([buffer]);
const url = URL.createObjectURL(blob);
downloadLink.href = url;
});
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
const recorder = new MediaRecorder(stream);
recorder.ondataavailable = (e) => {
if (e.data.size > 0) socket.emit("audio:chunk", e.data);
};
recorder.start();
});
For large file transfers, chunk the data manually and reassemble on the receiver. Do not send files larger than a few MB as single events — it saturates the event loop.
Example
A real-time presence and messaging system:
export function registerPresenceHandlers(io: Server, socket: Socket) {
socket.on("presence:ping", (roomId: string) => {
socket.to(roomId).emit("presence:online", socket.data.userId);
});
socket.on("disconnecting", () => {
for (const room of socket.rooms) {
if (room !== socket.id) {
io.to(room).emit("presence:offline", socket.data.userId);
}
}
});
}
function useSocket(roomId: string) {
const [online, setOnline] = useState<string[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>([]);
useEffect(() => {
socket.emit("chat:join", roomId);
socket.emit("presence:ping", roomId);
socket.(, ( [...prev, msg]));
socket.(, ( [... ([...prev, id])]));
socket.(, ( prev.( u !== id)));
{
socket.(, roomId);
socket.();
socket.();
socket.();
};
}, [roomId]);
= () => socket.(, { roomId, text });
{ online, messages, send };
}