100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Presence and Online Status

Implement accurate multi-device presence tracking in Socket.IO using reference counting and a Redis-backed store for horizontal scaling.

Practical Socket.IOIntermediate8 min readJul 10, 2026
Analogies

Presence and Online Status

Presence tracking answers a simple question — 'who is online right now?' — but implementing it correctly requires more than listening for connection and disconnect, because a single user can have multiple sockets open (two browser tabs, a phone and a laptop), and naively setting a user to offline on any disconnect event will incorrectly mark them offline while their other tab is still connected. The standard fix is a reference-counted presence map, typically stored in Redis, where connecting increments a counter for that user and disconnecting decrements it, only firing the 'user went offline' broadcast when the count reaches zero.

🏏

Cricket analogy: It's like a scorer only declaring a batsman 'out' after all forms of dismissal are confirmed by the third umpire — a single misleading replay angle (one closed tab) shouldn't overturn the on-field call if other evidence (another open tab) still shows the batsman in.

Multi-Device Presence with a Reference Count

In practice, this reference count lives outside the Node process — a Redis hash or sorted set keyed by userId works well because it survives server restarts and works correctly across multiple horizontally scaled Socket.IO servers, which is essential once you're running more than one instance behind a load balancer. On connection, the handler runs something like INCR presence:<userId>, and on disconnect it runs DECR presence:<userId>, checking if the result is zero before broadcasting user:offline; on the increment side, if the pre-increment value was zero, that's the signal to broadcast user:online.

🏏

Cricket analogy: It's like a stadium's turnstile counter tracking exactly how many members of a touring squad have entered the ground — the gate only reports 'team fully departed' once the counter returns to zero, not after the first player leaves for a press conference.

Scaling Presence Across Multiple Server Instances

A naive in-memory Map for presence breaks the moment you run two Socket.IO server processes, because a user's two tabs might connect to different instances, and neither process alone knows the true count; this is exactly why Socket.IO's Redis adapter (@socket.io/redis-adapter) exists — it uses Redis pub/sub to relay emit calls across all instances so io.to(room).emit() reaches sockets connected to any server in the cluster, and presence counters built on Redis (as opposed to per-process memory) are correct regardless of which instance a given tab lands on.

🏏

Cricket analogy: It's like two separate broadcast trucks covering the same match from different ends of the ground — without a shared production feed (Redis) linking them, one truck has no idea what replay the other just aired, and viewers get an inconsistent picture.

javascript
// Redis-backed presence with reference counting
const { createClient } = require('redis');
const { createAdapter } = require('@socket.io/redis-adapter');

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

io.on('connection', async (socket) => {
  const userId = socket.data.userId;
  const count = await pubClient.incr(`presence:${userId}`);
  if (count === 1) {
    io.emit('user:online', { userId });
  }

  socket.on('disconnect', async () => {
    const remaining = await pubClient.decr(`presence:${userId}`);
    if (remaining <= 0) {
      await pubClient.del(`presence:${userId}`);
      io.emit('user:offline', { userId, lastSeen: Date.now() });
    }
  });
});

A disconnect event fires on network drops, browser crashes, or the user simply closing their laptop lid — not only clean logouts. Always pair presence with a heartbeat or Socket.IO's built-in ping/pong timeout (pingTimeout, pingInterval) rather than assuming disconnect is only ever intentional, and consider a short grace period before marking a user fully offline to smooth over brief reconnects like a mobile network handoff.

  • Naively toggling presence on connection/disconnect breaks for users with multiple simultaneous sockets (tabs, devices).
  • A reference-counted presence map, incremented on connect and decremented on disconnect, correctly handles multi-device users.
  • The presence counter should live in a shared store like Redis, not per-process memory, so it survives restarts and horizontal scaling.
  • The Redis adapter (@socket.io/redis-adapter) relays emits across all server instances via pub/sub, enabling correct cross-instance broadcasting.
  • Broadcast user:online only when the count transitions from zero, and user:offline only when it returns to zero.
  • Disconnects can be unintentional (network drop, crash), so a grace period before declaring a user offline avoids status flapping.
  • Socket.IO's ping/pong heartbeat settings (pingInterval, pingTimeout) control how quickly a dead connection is detected as disconnected.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SocketIOStudyNotes#PresenceAndOnlineStatus#Presence#Online#Status#Multi#StudyNotes#SkillVeris#ExamPrep