"Who is online" sounds trivial until you have several WebSocket servers behind a load balancer and a client that vanishes without a disconnect. The hard part of presence isn’t join — it’s leave.
The naive set, and why it rots
A plain SADD online <userId> works right up until a process is killed. The member is never removed, and your "online" set slowly fills with ghosts.
presence.tsTypeScript
// One key per connection, with a TTL. No goodbye required.
const key = `presence:${userId}:${connId}`
await redis.set(key, '1', 'EX', 30) // expires in 30s
// Heartbeat from the client refreshes it
setInterval(() => redis.expire(key, 30), 10_000)
// "Is the user online?" = does any connection key still exist?
const online = (await redis.keys(`presence:${userId}:*`)).length > 0Don’t KEYS in production
KEYS scans the whole keyspace and blocks the server. Use SCAN, or keep a per-user SET of connection ids and let the TTL keys be the source of truth a background sweeper reconciles.
What the TTL buys you
- A crashed server’s connections expire on their own — no cleanup hook needed
- Presence is eventually consistent within one heartbeat interval
- Works identically across any number of socket servers
- No dependency on a framework’s built-in adapter
Never build presence on the assumption that clients say goodbye. Build it on the assumption that they don’t.
Heartbeats and expiry turn an unreliable network into a self-healing one. The dead clean up after themselves.