Designing Chat Presence: Online, Away, and the Lie of Instant Status
Every chat mock starts with a green dot. The naive model is a SET online:{userId} in Redis, written on WebSocket connect and deleted on disconnect. That model dies the moment you have multiple devices, flaky mobiles, a privacy setting, and a friends list of 800 people each opening a channel. Presence is a derived signal with a freshness SLA, not a row in a users table.
The signal, not the socket
A user is not "online" because a TCP connection exists. They are online if some device has sent a heartbeat within T seconds (typically 30–90s). Disconnect is not reliable — NAT timeouts, laptops asleep, load balancers that do not send a close. Design for expiry: each device holds a key presence:{userId}:{deviceId} with TTL T, refreshed on heartbeat. The user is online if any device key exists. Away is a client-declared state or a last-input timestamp older than a threshold while the connection still lives.
Store last-seen as a separate field updated on heartbeat downsample (every Nth beat, or at most once per minute) so a presence storm does not become a write storm to the profile store.
Fan-out is the real problem
The expensive part is not knowing Alice is online. It is telling everyone who cares. If you publish alice_online to a global pub/sub, every session on the fleet evaluates "do I care?" — that does not scale.
Two workable shapes:
Pull on viewport. The client already knows which dots are on screen (inbox, conversation header). It subscribes to presence for those ids, not the entire graph. When the user scrolls the inbox, the subscription set changes. This is how large messengers stay cheap: presence is a cache around the current UI, not a firehose of the social graph.
Push to small rooms. For a group chat of 12, the room's presence channel is tiny; broadcast is fine. For a user with 5,000 followers, do not notify followers that they opened the app. Product-wise, "online to friends" is already a privacy subset; "online to the internet" is a notification DDoS.
A hybrid that shows up in production: write presence to a regional Redis cluster; gateway nodes subscribe to the ids their connected clients currently watch; a membership service maps user → interested viewers only for 1:1 threads and small groups.
Consistency you can explain
Presence is allowed to be wrong for a few seconds. Users accept a stale green dot; they do not accept a message that says "offline" while they are mid-conversation. Prefer sticky online: once marked online, wait out the TTL before flipping offline, even if one of three devices dropped. Prefer fast online: the first heartbeat of a session should light the dot within a second for people already looking at that thread.
Do not put presence in the same strongly consistent store as messages. A presence blip must never block or reorder chat. If Redis is down, degrade: hide dots, keep last-seen from the last successful downsample, do not freeze the inbox.
Multiple devices and privacy
Device keys let phone and desktop independently expire. The UI usually shows one user-level state: online if any device is, else last-seen from the most recent device. "Active on mobile" is a product extra, not a storage extra — you already have deviceId.
Privacy settings belong in the read path. Alice can be online-to-Bob and invisible-to-Carol. The presence service returns a per-viewer projection, not a global boolean. Caching that projection is tempting and dangerous: a block list change must invalidate presence_view:{viewer}:{subject} or you leak a green dot after a block. Keep the authoritative check next to the subscription grant ("may viewer subscribe to subject?") so the cache cannot outlive the permission.
Capacity sketch
Assume 10M DAU, 10% concurrently connected, heartbeat every 45s: ~22k presence writes per second if you write every beat. Downsample heartbeats in the gateway (refresh TTL locally, hit Redis every 15–20s) and you cut that by 2–3×. Subscriptions: if each client watches 50 ids, that is 50M watch relationships at peak — too many for naive per-id pub/sub across a global bus. Shard by subject id, keep watches in the region that holds the socket, and never subscribe a client to the whole friend graph.
The interview-grade close is this: presence is an eventually consistent cache with TTL, a narrow fan-out, and a privacy function on read. The green dot is a UX lie we tell carefully. The system underneath is heartbeats, expiry, and refusing to broadcast the whole world.
Keep reading
Consistency Models Beyond CAP: Linearizability, Causal, and Session Guarantees
'Strong vs eventual' is a cartoon. The real spectrum — linearizable, sequential, causal, session guarantees — and how to pick per operation, not per system.
Designing an API Gateway: The Front Door as a System
Authentication, routing, rate limits, transformations, and a plugin chain — in a tier that must add ~1ms and never be the outage. Envoy/Kong architecture from scratch.
Designing a CDC Pipeline: Change Data Capture from Binlog to Downstream
Dual writes are a lie; the database's own log is the truth. Log-based CDC, the snapshot-plus-stream handoff, schema evolution, and ordering guarantees that survive resharding.
Designing a Content Moderation Pipeline: Trust and Safety at Platform Scale
A billion uploads a day, a legal requirement to act in hours, and irreversible mistakes in both directions: hash matching, classifier tiers, human review queues, and appeals.
Designing a Distributed Rate Limiter: Local Buckets, Global Truth
One user, twenty gateway nodes, one limit of 100 req/s. Where does the counter live? Redis+Lua atomicity, the local-cache compromise, and failing open vs closed.
Designing a Durable Workflow Engine: How Temporal Makes Code Survive Crashes
Event-sourced workflow state, deterministic replay, task queues with sticky caches, and why 'just write normal code' is the hardest API promise in infrastructure.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.