List the key functional requirements for the system (Ask the AI for hints if stuck)...
Real-time presence tracking: Track each user as online, idle, or offline. A user connects and becomes online. After a period of inactivity they transition to idle. When they disconnect or miss heartbeats they transition to offline. The system maintains a state machine per user with well-defined transitions.
Push-based status updates: When a user's status changes, notify all friends currently watching within one second. Friends see a green dot appear or disappear in real time without refreshing.
Multi-device aggregation: A user may be on their phone, laptop, and tablet simultaneously. The aggregate status is online if any device is connected. Only when the last device disconnects does the user appear offline.
"Last seen" timestamps: For offline users, display when they were last active. This requires persisting the timestamp of the most recent heartbeat before the user went offline.
Visibility controls: Users can restrict who sees their presence. An "appear offline" mode suppresses all status updates to friends.
List the key non-functional requirements (performance, scalability, reliability, etc.)...
Scale: 500 million registered users, 100 million daily active, 10 million concurrent online at peak.
Throughput: 330,000 heartbeats per second. Status change fan-out of up to 330,000 notifications per second during peak transitions.
Latency: Status change propagation to friends in under one second.
Availability: 99.99% uptime. A user appearing offline when they are actually online is a worse failure than a one-second delay in updating status.
Consistency: Eventual consistency is acceptable. A friend seeing "online" for a few seconds after a user disconnects causes no harm. But the system must converge within seconds, not minutes.
Estimate the scale of the system. Consider daily active users, read/write ratio, storage requirements, bandwidth, and any relevant QPS calculations...
500 million registered users, 100 million daily active, 10 million concurrent online at peak. Average friend count is 200.
Heartbeat rate: 10M users / 30s interval = 330K heartbeats per second.
Status changes: Roughly 1% of online users change status per minute (connect or disconnect). That is 100K status changes per minute, or about 1,700 per second.
Fan-out from status changes: Each status change notifies an average of 200 friends. 1,700 changes/sec times 200 friends = 340,000 notifications per second at peak.
Live presence state (Redis): Each user entry is roughly 100 bytes (user ID, status, last heartbeat timestamp, device list). 10 million concurrent users times 100 bytes = 1 GB. This fits comfortably in a single Redis instance but requires replication for availability.
Friend lists (cached): 10 million concurrent users times 200 friends times 8 bytes per friend ID = 16 GB. This is cached in Redis for fast fan-out lookups.
Last-seen timestamps (PostgreSQL): 500 million users times 16 bytes (user ID plus timestamp) = 8 GB. Updated on each offline transition. Read-heavy, write-infrequent.
Define the APIs expected from the system. This is your chance to analyze and define the read and write paths so that you can come up with the high-level design...
wo communication patterns serve different needs. WebSocket carries the continuous heartbeat and push notification traffic. REST handles discrete queries and settings changes. Sending 330K heartbeats per second over HTTP would create 330K TCP handshakes per second, WebSocket amortizes the connection cost across thousands of heartbeats.
Client heartbeat (sent every 30 seconds): { "type": "heartbeat", "device_id": "d_abc123", "ts": 1709567890 }
Server status push (sent when a friend's status changes): { "type": "status_change", "user_id": "u_789", "status": "online", "ts": 1709567891 }
The WebSocket connection serves dual purpose: the client sends heartbeats to maintain its own presence, and the server pushes friend status changes through the same connection. No separate subscription mechanism is needed. If a client has an open WebSocket, it is both reporting liveness and receiving updates.
GET /v1/users/:user_id/presence, Query a single user's current status and last-seen timestamp. Used for profile pages and initial state loading when a chat window opens.
GET /v1/users/presence?ids=u_1,u_2,u_3, Batch query for multiple users. Used when loading a contact list to display status indicators for all visible friends at once. Accepts up to 200 IDs per request.
PUT /v1/users/:user_id/presence/settings, Update visibility preferences (appear offline, restrict to specific friend lists). Requires authentication. Changes take effect immediately by suppressing future status change events.
GET /v1/users/:user_id/presence/history?from=ts&to=ts, Retrieve presence history for analytics dashboards. Returns timestamped status transitions. Rate-limited to prevent abuse.
Describe the overall system architecture. Identify the main components needed to solve the problem end-to-end. Use the diagramming tool to create a block diagram.
The key has a TTL of 60 seconds. Every heartbeat resets the TTL. If two consecutive heartbeats are missed (60 seconds of silence), the key expires and any read of the user's presence returns offline. Note what the TTL does and does not do: key expiry in Redis is silent, nobody is notified, so the TTL alone can never drive fan-out to friends. Active offline detection (noticing the timeout and pushing notifications) is the job of the Presence Service's timing wheel, covered in the deep dive, which declares offline after roughly 40 seconds (30 seconds to the missed heartbeat plus a 10-second grace period). The TTL is the passive backstop behind it: it guarantees reads never return stale online state even if the Presence Service instance holding a user's timers crashes, and it cleans up state automatically with no background scan. The 60 second TTL sits deliberately outside the 40-second active deadline so the wheel fires first in normal operation.
Why a hash instead of a simple string? The hash stores per-device state. When a heartbeat arrives from a phone, the service updates only the phone's entry within the hash without touching the laptop's entry. Multi-device aggregation reads all device fields and computes the aggregate status.
WebSocket Gateway: Accepts and maintains persistent client connections. Receives heartbeats and forwards them to the Presence Service. Subscribes to Redis Pub/Sub channels for friend status updates and pushes them to connected clients. Stateless except for the connection registry (which clients are connected to this gateway instance).
Presence Service: The brain of the system. Receives heartbeats, updates Redis state, manages the state machine (online, idle, offline transitions), and triggers fan-out when status changes. Runs timeout detection using a timing wheel to efficiently identify users who have missed heartbeats. Durable state lives in Redis, but each instance also holds an in-memory acceleration layer (per-user actors, grace timers, and timing wheel slots, covered in the deep dive), so heartbeats are routed to a user's owning instance via consistent hashing. That in-memory state is a rebuildable cache of Redis, which keeps instance failure cheap.
Redis: Serves three roles. First, presence state store (hashes with TTL for each online user). Second, friend list cache (sets of friend IDs for online users). Third, pub/sub backbone (channels for distributing status change events to gateway instances).
Friend Service: Owns the friendship graph in PostgreSQL. Provides friend list lookups used during fan-out. Caches hot friend lists in Redis on first access. Called infrequently, only when a user connects (to warm the cache) or when the friend list changes.
Notification Router: Resolves which gateway instance each friend is connected to. Maintains a mapping of user ID to gateway instance in Redis. When a status change needs to fan out to 200 friends, the router groups friends by gateway and sends one batched message per gateway instead of 200 individual messages.
HSET presence:u_123 phone_last_hb 1709567890 and EXPIRE presence:u_123 60 in a Redis pipeline (two commands, one round trip).Total latency: under 1ms for the common case. Redis HSET plus EXPIRE in a pipeline takes roughly 0.2ms.
SMEMBERS friends:u_123). If cache miss, loads from Friend Service and caches.HMGET gateway_map friend_1 friend_2 ... friend_200) to determine which gateway each friend is connected to. Friends who are offline (not in the gateway map) are skipped.Total latency: 50-200ms from status change detection to delivery at the furthest gateway. The friend list read (0.5ms) plus gateway map read (1ms) plus message delivery (10-50ms per gateway) fits well within the one-second budget.
Define the data model. Identify the main entities, their attributes, and relationships. Consider the choice of database type (SQL vs NoSQL) and justify your decision based on access patterns...
Each online user gets a hash in Redis:
Key: presence:u_12345 Fields: status (online/idle/offline), last_heartbeat (epoch ms), devices (JSON array of device IDs and their individual statuses)
Key: friends:u_12345 Value: Set of friend user IDs
Loaded from PostgreSQL when a user connects. Used during fan-out to determine which friends to notify. Evicted when the user disconnects. Only online users have their friend lists cached. This keeps memory at 16 GB (10M users times 200 friends times 8 bytes) instead of 800 GB (all 500M users).
users table: user_id (PK), username, created_at
friendships table: user_id, friend_id, created_at, bidirectional (two rows per friendship). Indexed on user_id for fast friend list retrieval.
presence_settings table: user_id (PK), appear_offline (boolean), visibility_mode (enum: everyone, friends_only, nobody), updated_at
last_seen table: user_id (PK), last_online_at (timestamp), updated when a user transitions to offline. Read when displaying "Last seen 5 minutes ago" on profiles.
Deep dive into 2-3 key components. Explain how they work, how they scale, discuss tradeoffs, capacity, and any relevant algorithms or data structures.
"How do you detect that a user went offline?" is the question that separates surface-level answers from deep understanding. Three mechanisms compete: heartbeat timeout, explicit disconnect, and TCP connection drop. The challenge is avoiding false transitions. A user on a flaky mobile network should not flicker between online and offline every 10 seconds.
When a heartbeat is missed (the timing wheel slot expires), the system does not immediately declare the user offline. Instead, a grace period of 10 seconds begins. If a heartbeat arrives during the grace period, the transition is cancelled and the user stays online. If no heartbeat arrives, the user transitions to offline.
Why 10 seconds? Mobile networks routinely experience 5-7 second connectivity gaps (cell tower handoffs, tunnel passages, elevator rides). A 10-second grace period absorbs these transient losses. The trade-off: genuine disconnects take 10 extra seconds to detect. But the alternative, flapping between online and offline on every network hiccup, is worse for user experience.
The idle transition works differently from the offline transition, and the distinction matters. While the app is open, heartbeats keep flowing, so the server cannot tell online and idle apart from heartbeat presence alone. Instead, the client tracks the user's last input locally (mouse, keyboard, touch). After five minutes with no input, the client sets an idle flag in its heartbeat payload. The actor sees the flag, marks that device idle, recomputes the aggregate status, and triggers the batched idle fan-out if the aggregate changed. When input resumes, the next heartbeat clears the flag and the user snaps back to online.
Offline is the opposite case: a crashed or disconnected client cannot report anything, so the server must infer offline from heartbeat silence using the timing wheel and grace period. In short: idle means still heartbeating but flagged inactive, offline means no heartbeats at all. This is why idle detection needs no timers on the server and never involves the TTL or the timing wheel.
A user has three devices: phone (device_1), laptop (device_2), tablet (device_3). Each device sends heartbeats independently. The actor maintains a map of device statuses:
{ device_1: online, device_2: online, device_3: idle }
The aggregate status follows a simple rule: if any device is online, the user is online. If all devices are idle, the user is idle. If no devices have active heartbeats, the user is offline.
When the phone disconnects, the actor updates device_1 to offline. The aggregate status remains online because device_2 is still active. No fan-out occurs, friends see no change. Only when all devices disconnect does the actor trigger the offline transition and fan-out.