Real-timeApril 30, 20261 min read

Building presence in Redis without the Redis adapter

Who’s online, across N socket servers, when a crash means a connection never says goodbye? Lean on TTLs, not goodwill.

"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 > 0
Don’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

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.

← All field notes