How do you continuously compute top active channels without scanning historical data?
Given message events across millions of channels, design a system that continuously computes the top active channels/users for the last 1 hour without scanning historical data, updating rankings within seconds of activity changes.
What the system must do · streaming top-K computation over a sliding window
Quality constraints for real-time ranking computation over sliding windows
| Property | Target | Why It Matters / Design Impact |
|---|---|---|
| Freshness | Rankings update within 5 seconds of activity | Stale rankings mislead users. Must reflect recent bursts immediately. |
| Query latency | P99 <50ms for top-K retrieval | Rankings are displayed in sidebar · must load faster than the page itself. |
| Throughput | 200K+ message events/sec ingested | Must keep up with full message stream without falling behind or dropping events. |
| Accuracy | Approximate acceptable (·5% for ranking position) | Exact counts unnecessary · relative ordering is what matters for top-K display. |
| Window | Sliding 1-hour window with automatic expiry | Old activity must decay without explicit cleanup. Time-bucketed approach enables this. |
| Scalability | Per-workspace rankings + global aggregation | Each workspace is independent. Global view aggregates across workspaces on demand. |
| Memory | Bounded regardless of total channel count | Only active channels consume memory. Inactive channels naturally expire from sorted sets. |
Deriving Redis capacity from event rate and workspace distribution
| Step | Calculation | Result |
|---|---|---|
| 1 | 200K msgs/sec from Kafka → 200K ZINCRBY ops/sec to Redis | 200K write ops/sec |
| 2 | 60 time buckets · avg 10K active channels/workspace | 600K ZSET entries |
| 3 | Memory: 600K entries · 16 bytes | ~10MB per workspace (trivial) |
| 4 | ZUNIONSTORE across 60 keys: O(60 · 10K) | 600K ops every 5s |
| 5 | Total workspaces: 500K · 10MB | ~5TB if all active (but most are small) |
| 6 | Realistic: top 1000 workspaces need dedicated Redis | Rest share multi-tenant clusters |
Kafka stream → sliding window aggregation → Redis sorted sets with time-decay scoring → top-K query
Streaming computation with time-bucketed aggregation · no batch jobs, no historical scans
| Decision | Choice | Why | Alternative Rejected |
|---|---|---|---|
| Window Strategy | Time-bucketed (1-min buckets · 60) | Each minute gets its own Redis sorted set. Union 60 buckets = 1-hour window. Old buckets auto-expire via TTL. | Single counter with decay: hard to expire exactly at 1hr. Tumbling window: stale for up to window-size. |
| Storage | Redis Sorted Sets (ZSET) | ZINCRBY O(log N) for increment. ZUNIONSTORE for aggregation. ZREVRANGE for top-K. All sub-ms operations. | Database: too slow for 200K writes/sec. In-memory only: lost on restart (acceptable for rankings). |
| Expiry | TTL on bucket keys (3660s) | Redis auto-deletes expired buckets. No background cleanup job needed. Sliding window maintained passively. | Manual cleanup cron: adds operational complexity. Explicit delete: race conditions with writers. |
| Accuracy | Exact per-workspace, approximate global | Per-workspace has <10K channels → Redis ZSET is exact. Global across millionS → CMS + heap is approximate but bounded. | Exact global: requires scanning all workspaces. Too expensive for real-time. |
| Computation Model | Streaming (no batch) | Every message event immediately increments the counter. No periodic batch job. Rankings are always fresh within 5s. | Batch (every 5 min): stale rankings, spiky compute load. Lambda architecture: unnecessary complexity. |
Rankings are ephemeral and rebuildable · the system optimizes for freshness over durability
| # | Failure / Edge Case | What Breaks | How It's Handled |
|---|---|---|---|
| 1 | Redis node failure | Current bucket data lost · rankings temporarily inaccurate | Rankings rebuild within 1 minute as new events flow in. Redis Sentinel promotes replica. Stale data acceptable for rankings. |
| 2 | Kafka consumer lag | Rankings lag behind real activity | Alert on lag >30s. Auto-scale consumers. Rankings are eventually consistent · 30s staleness is acceptable. |
| 3 | Bot spam in a channel (10K msgs/min) | Bot-spammed channel dominates rankings | Rate-limit per user at ingestion. Optionally: weight by unique users, not raw message count. Filter known bots. |
| 4 | ZUNIONSTORE on 60 keys is slow | Query latency spikes during aggregation | Pre-compute union every 5s into a result key. Clients read from cached result. ZUNIONSTORE runs in background. |
| 5 | Workspace with 100K+ channels | ZSET per bucket becomes large | Only track channels with >0 messages in the bucket. Most channels are inactive · ZSET stays small. Worst case: shard across Redis nodes. |
Minimal stack · streaming aggregation with Redis as the computation engine
| Component | Technology | Why This | Why Not X |
|---|---|---|---|
| Event Source | Kafka (messages topic) | Same topic as message delivery. Ranking is just another consumer group. No additional event production needed. | Separate event stream: duplicates data, adds complexity. CDC from DB: higher latency. |
| Stream Processor | Custom consumer (Go/Java) | Simple logic: extract workspace+channel, ZINCRBY Redis. No need for Flink/Spark complexity. | Flink: powerful but operationally heavy for simple counting. Spark Streaming: batch-oriented, higher latency. |
| Aggregation Store | Redis Sorted Sets | ZINCRBY for counting, ZUNIONSTORE for window aggregation, ZREVRANGE for top-K. All O(log N) or better. | TimescaleDB: too slow for 200K writes/sec. ClickHouse: batch-oriented, not real-time enough. |
| Approximate Counting | Count-Min Sketch (for global) | Fixed memory (~1MB) regardless of cardinality. Bounded error. Perfect for "approximate top-K" requirement. | Exact counting at global scale: requires O(channels) memory. HyperLogLog: counts uniques, not frequencies. |
| Result Cache | Redis key with 5s TTL | Pre-computed top-100 result. Clients read cached result. Background job refreshes every 5s. | Compute on every query: ZUNIONSTORE on 60 keys per request is wasteful. CDN: too aggressive caching. |
The 5 things an interviewer wants to hear about real-time ranking systems