Designing a real-time active user counter
How to count active users in real time using sliding windows, HyperLogLog, and distributed counter aggregation while handling millions of concurrent heartbeats.
The Problem Statement
Interviewer: "Your product manager walks up and says: 'I want to show a live active user count on the homepage. Something like 1.2M users online right now.' How would you build that? What does 'active' even mean technically, and how do you count millions of unique users in real time without melting your database?"
This question tests three things: whether you can pin down an ambiguous requirement ("active" is not a technical term), whether you understand probabilistic data structures like HyperLogLog, and whether you can design a write-heavy pipeline that absorbs millions of heartbeats per second without becoming the bottleneck.
This question is useful because the naive answer is obvious (query the database), while the real challenge is recognizing why that breaks at scale. The distance between the naive solution and a production-ready design is large, and every step along that path reveals an architectural trade-off.
The same pattern appears in YouTube's "watching now" counter, Twitch's viewer count, Slack's online indicator count, and dashboard analytics showing concurrent sessions. Solve this once and you have a reusable mental model.
Clarifying the Scenario
You: "Before I start designing, I want to nail down what 'active' means. There are at least three reasonable definitions, and they lead to very different systems."
Interviewer: "What definitions are you thinking?"
You: "First, 'active right now' could mean the user has the app open and has interacted in the last 5 minutes. Second, it could mean they have an open connection, like a WebSocket or SSE stream. Third, it could mean they have logged in at some point today, which is really DAU, not real-time active."
Interviewer: "Let us go with the first one. Users who have sent a heartbeat within the last 5 minutes."
You: "Great. And what accuracy do we need? Is showing '~1.2M' acceptable, or do we need the exact number 1,247,893?"
Interviewer: "Approximate is fine. Within 1-2% error."
You: "That is a crucial constraint because it unlocks HyperLogLog, which gives us sub-1% error with 12KB of memory per counter. And one more: are we multi-region? Because if users are distributed across US, EU, and Asia, each region will have a local count that needs to be merged into a global total."
Interviewer: "Yes, assume three regions."
You: "Perfect. I will structure my answer around three parts: how we ingest heartbeats at scale, how we count unique users using HyperLogLog with time buckets, and how we aggregate across regions for a global display number."
Why the definition matters so much
"Active in the last 5 minutes" and "active today (DAU)" are completely different systems. DAU is a batch job that runs once a day. Real-time active is a streaming system that must process millions of events per second continuously. Getting the PM to clarify this up front saves you from designing the wrong thing entirely.
My Approach
I break this into five parts:
- The heartbeat model: How clients report liveness to the server
- Write absorption: How to handle millions of heartbeats/second without overloading storage
- Unique counting with HyperLogLog: Why exact COUNT DISTINCT does not scale, and what to use instead
- Time-bucketed sliding windows: How to answer "active in the last 5 minutes" without rescanning all data
- Multi-region aggregation: How to merge regional counters into a global number
The core insight is that this is not a counting problem. It is a cardinality estimation problem. You do not need to know the exact number. You need a number that is close enough to display on a dashboard. Once you internalize that, the entire system simplifies dramatically because you stop trying to track every individual user and start using probabilistic structures that give you the answer in constant time and constant space.
Think of it like estimating the size of a crowd at a concert. You do not count every person. You estimate using density sampling: count the people in one square meter, count the square meters, multiply. The answer is not exact, but it is close enough and it takes seconds instead of hours.
Numbers at a glance
| Metric | Approximate value |
|---|---|
| Total registered users | 500 million |
| Peak concurrent active users | 5-10 million |
| Heartbeat interval | 30 seconds per client |
| Heartbeats per second (peak) | ~300K (10M users / 30s) |
| HyperLogLog memory per counter | 12 KB |
| HyperLogLog standard error | 0.81% |
| Time bucket granularity | 1 minute |
| Sliding window size | 5 minutes |
| Display refresh interval | 10 seconds |
| Regions | 3 (US, EU, Asia) |
The Architecture
Here is the full system, from client heartbeat to the displayed counter on the homepage.
Let me walk through this step by step.
The client sends a lightweight heartbeat every 30 seconds. This is just a POST request with the user ID and a timestamp. Nothing else. No session data, no activity details. Just "I am alive."
The load balancer fans these out to Kafka. Kafka can be used here instead of writing directly to Redis because it acts as a shock absorber. If a sudden spike occurs (10M users all heartbeat within the same second after a push notification), Kafka buffers the writes and consumers drain at a sustainable rate. Without buffering, a spike could overload Redis with millions of simultaneous PFADD commands.
The consumer group reads batches of heartbeats and runs PFADD on the appropriate minute-bucket HyperLogLog in Redis. The aggregator runs every 10 seconds, merges the last 5 minute-buckets using PFMERGE, and stores the result. The Counter API reads this merged value and serves it with a 10-second cache TTL.
The key insight: the homepage never touches Kafka, never touches the raw heartbeats, never scans a database. It reads a single pre-computed integer from Redis. The entire write pipeline exists to keep that single number fresh.
This is the answer that impresses
Most candidates describe the heartbeat model and then immediately say "query the database." The strong answer separates the write path (absorb heartbeats into Kafka, funnel into HLL) from the read path (serve a pre-computed number from Redis). This separation of concerns is what makes the system scale.
Deep Dive 1: HyperLogLog for Approximate Unique Counting
The central challenge is counting unique users. If 10 million users each send a heartbeat every 30 seconds, we get ~300K heartbeats per second. But many of those are the same user heartbeating again. We need the count of distinct users, not the count of heartbeats.
Let me walk through why each approach fails or succeeds.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.