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.
// 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/disconnectbreaks 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:onlineonly when the count transitions from zero, anduser:offlineonly 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
1. Why is a simple on/off flag per user insufficient for presence tracking?
2. What is the purpose of the Socket.IO Redis adapter?
3. When should a `user:online` event be broadcast in a reference-counted presence system?
4. Why might a `disconnect` event fire even though the user didn't intentionally log out?
Was this page helpful?
You May Also Like
Building a Chat Application
Learn how to design a real-time chat app with Socket.IO, covering rooms, broadcasting, message persistence, and delivery guarantees.
Real-Time Notifications
Design a reliable real-time notification system with Socket.IO using per-user rooms, namespaces, and durable fan-out for offline users.
Testing Socket.IO Apps
Write reliable integration and unit tests for Socket.IO features using real servers, real clients, and targeted mocking strategies.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics