← The blog
Infrastructure·Sep 2025·8 min read

WebSocket live sessions with Socket.io: presence and reconnects

Presence keyed by user with a reference count, plus a Redis adapter so two server instances share one room.

A user with two tabs open is one person, not two, and closing one tab should not show them as offline. Getting presence right means counting connections per user, not per socket. Add a Redis adapter so it works across server instances, and reconnects stop being a source of ghost users.

Count sockets, report users

Presence is a map from user id to a set of their socket ids. A user is online while that set is non-empty. Two tabs add two sockets, closing one leaves them online, closing both marks them away.

io.on("connection", (socket) => {
  const uid = socket.data.userId;
  add(uid, socket.id);
  if (size(uid) === 1) broadcast("presence", { uid, status: "online" });

  socket.on("disconnect", () => {
    remove(uid, socket.id);
    if (size(uid) === 0) broadcast("presence", { uid, status: "offline" });
  });
});

Reporting per socket instead of per user is the classic bug, where a user flickers offline every time they open a second tab.

One room across two instances

Socket.io's default adapter keeps rooms in the memory of a single process. Run two instances behind a load balancer and a message emitted on instance A never reaches a client connected to instance B. The Redis adapter fixes that by publishing emits over Redis so every instance delivers them.

import { createAdapter } from "@socket.io/redis-adapter";
io.adapter(createAdapter(pubClient, subClient));

With the adapter in place, a chat message or a presence change reaches every relevant client regardless of which instance they happen to be pinned to.

Sticky sessions still matter

The Redis adapter shares messages, but the initial handshake and any long-polling fallback still need the client to keep hitting the same instance. The load balancer needs sticky sessions for that, otherwise the upgrade from polling to WebSocket can land on the wrong box and fail. The adapter and stickiness solve different halves of the same problem.

Reconnects without duplicates

A flaky network drops and restores the connection constantly. Socket.io reconnects automatically, but the server has to treat a reconnect as the same user resuming, not a new arrival. Because presence is keyed by user id from the authenticated session rather than the socket, a reconnect cleanly replaces the old socket id with the new one.

socket.on("disconnect", (reason) => {
  if (reason === "transport close") scheduleAway(uid, 5000);
});

A short grace period before marking someone away absorbs the typical one or two second blip, so a subway tunnel does not light up the room with leave and join noise. Count per user, share state through Redis, keep sessions sticky, and forgive brief drops. Those four together are the difference between a presence indicator people trust and one they learn to ignore.

Socket.ioWebSocketsRedisRealtime
Let's talk

Let's build
SOMETHING REAL.

Have a system that needs to think, scale, or just finally get shipped? Let's make it happen.

Based in Dhaka, Bangladesh · Available worldwide