Design a Feature to Show the Number of Users Viewing a Page — System Design Interview Practice
Design a real-time presence feature that shows how many users are currently viewing the same page without turning page views into durable chatty writes. Work through the requirements, architecture trade-offs, and an interactive design review.
Concepts and architecture decisions to consider
- real timeConcept to explore
- presenceConcept to explore
- websocketsConcept to explore
- scalabilityConcept to explore
Interview prompt
Design a feature that shows how many users are viewing the same page right now, while remaining useful under reconnects, duplicate tabs, mobile sleep, hot pages, and a large number of concurrent viewers.
- Define whether the count is exact, approximate, or eventually consistent and show its freshness.
- Use expiring leases and idempotent session identities instead of one permanent write per page view.
- Separate heartbeat ingestion from count broadcast and keep analytics off the live path.
- Plan for hot rooms, reconnect storms, browser sleep, and regional failure explicitly.
Requirements and scale assumptions
- Join a page presence room when a page is opened and leave it when the session closes.
- Show a count that updates while the page remains open, with a freshness timestamp or stale indicator.
- Count one logical session once even if heartbeats are duplicated or retried; optionally merge authenticated tabs by policy.
- Handle disconnects, browser sleep, navigation, reconnects, and page deletion safely.
- Keep count updates below 2 seconds under normal load and expire abandoned sessions without client leave events.
- Avoid durable per-heartbeat writes; a presence-store failure must not corrupt business data.
- Support privacy controls, origin validation, authentication where required, and per-connection quotas.
- Degrade to a stale/approximate count rather than blocking page rendering when the live service is unavailable.
- 50 million daily active viewers and 5 million concurrent page sessions at peak.
- Heartbeats every 15 seconds with a 45-second lease; assume 330K heartbeat messages per second at peak.
- Most rooms have fewer than 100 viewers, but the hottest 100 pages can each reach 100K viewers.
- Count changes are coalesced for 250ms and analytics samples 1 in 100 presence events.
- Heartbeat rate: ~330K/s peak — 5M sessions renewing every 15 seconds; capacity must include reconnect bursts.
- Lease window: 45 seconds — Three heartbeat intervals tolerate a missed mobile tick while bounding stale presence.
- Count freshness: ≤2 seconds — The target for normal live updates; expose stale state when the budget is exceeded.
- Hot-room fan-out: 100K clients — Requires coalescing, shard-aware fan-out, and backpressure for popular pages.
Key entities
- PresenceSessionsessionId, userId, pageId, connectionId, lastHeartbeatAt, expiresAt
Ephemeral session lease for one viewer/page membership.
- RoomMembershippageId, sessionId, shard, expiresAt, status
Shard-local membership with expiry.
- PresenceSnapshotpageId, viewerCount, watermark, observedAt, approximate
Coalesced count projection with explicit freshness and approximation.
- HeartbeatsessionId, sequence, sentAt, receivedAt, connectionEpoch
Sequenced liveness signal used to reject stale reconnects.
Data flow
- 1. Open a presence sessionThe client obtains a stable session ID and connection epoch; the room router assigns a shard and records a short lease.
- 2. Refresh livenessHeartbeats extend the lease only when sequence and connection epoch are current; mobile sleep and duplicate tabs remain safe.
- 3. Expire and countShard owners remove expired memberships and publish coalesced deltas to a count projector rather than incrementing a global hot key.
- 4. Broadcast page updatesThe fanout path sends approximate or exact-enough count updates to subscribed clients with a watermark.
- 5. Reconnect and repairA reconnect reads the latest snapshot, resumes from a cursor where possible, and repairs shard/count drift asynchronously.
Deep dives and trade-offs
- Heartbeat leases and reconnect epochsFor the real-time page-presence feature, presence is disposable lease state, not a durable user fact, and expired sessions must not count. Design for the failure case where duplicate tabs, reconnect races, mobile sleep, and hot pages must not create unbounded fanout or phantom viewers; keep retries, versions, and repair state explicit. Expose freshness, version, lineage, or audit metadata so operators and clients can distinguish current, pending, and degraded state.
- Hot-page count aggregationFor the real-time page-presence feature, presence is disposable lease state, not a durable user fact, and expired sessions must not count. Keep this concern off unrelated request paths and partition it by the real-time page-presence feature access key. Expose freshness, version, lineage, or audit metadata so operators and clients can distinguish current, pending, and degraded state.
- Approximation and live deliveryFor the real-time page-presence feature, presence is disposable lease state, not a durable user fact, and expired sessions must not count. Keep this concern off unrelated request paths and partition it by the real-time page-presence feature access key. Expose freshness, version, lineage, or audit metadata so operators and clients can distinguish current, pending, and degraded state.
- Exact count versus bounded approximationUse shard-local membership and coalesced counts; expose approximation and freshness for hot pages. A globally exact synchronous counter creates a single hot key and couples every heartbeat to it.
- Heartbeat frequency versus battery/loadUse a moderate heartbeat interval, short lease extension, and server-side expiry; tune by device and page value. Very frequent heartbeats waste battery and connection capacity; very long leases overcount asleep clients.
- Push-only versus snapshot plus deltasPush coalesced updates but make a snapshot and cursor-based resync authoritative for reconnects. Push-only clients drift when mobile networks suspend sockets.