Designing for Scale · Real-Time Analytics
How to count billions of events in real-time without crushing your database, using stream processing, time-window aggregations, and Lambda architecture.
The problem this post solves
When a product launches, analytics is just a SELECT COUNT(*) on the production database. It works perfectly until the data grows, at which point the analytical queries start competing with the transactional queries for disk I/O, slowing down the entire application.
The standard fix is to move the data to a data warehouse (like Snowflake or Redshift) using a nightly batch job. This solves the performance problem but introduces a latency problem: the dashboard is always 24 hours out of date.
This post covers the architecture required to deliver real-time analytics—dashboards that update within seconds of an event occurring—at a scale where inserting every single event into a database is mathematically impossible.
First principles
- 1At high scale, events arrive at tens of thousands per second.forced by · Every user action (click, scroll, view) generates an event.
- 2Inserting ten thousand rows a second into a relational database causes severe write amplification and lock contention.forced by · B-Trees must constantly rebalance and update indexes.
- 3Even if you could write them, querying them requires scanning millions of rows per dashboard load.forced by · To show 'Views in the last 5 minutes', the DB must find and sum all the individual view rows.
- 4Scanning millions of rows per second for multiple concurrent dashboard users will saturate any disk array.forced by · I/O bandwidth is finite.
Architecture: The Lambda Architecture
The modern standard for handling both real-time and historical data is the Lambda Architecture, which splits the flow of data into two paths: the Speed Layer and the Batch Layer.
The Ingestion Layer (Kafka)
All events (clicks, views, purchases) are fired as fire-and-forget messages to a distributed event log (Kafka). Kafka is an append-only, sequential log. It can absorb millions of writes per second because it does almost no processing; it just writes bytes sequentially to disk.
The Batch Layer (The Source of Truth)
Kafka holds the data for a few days, but it also dumps all raw events into cheap, long-term storage (like Amazon S3). This is the Data Lake.
Every night, a massive batch job (using Spark) reads all the raw events from yesterday, cleans them, aggregates them, and loads them into a Data Warehouse. Advantage: It is perfectly accurate. It can handle late-arriving data. It can recalculate everything from scratch if a bug is found in the aggregation logic. Disadvantage: It is slow.
The Speed Layer (The Real-Time View)
While the Batch Layer is slowly processing yesterday's data, the Speed Layer is processing today's data as it happens.
A stream processing engine (like Apache Flink) reads the events from Kafka the millisecond they arrive. It does not store them. It keeps a running tally in memory.
- Memory usage is bounded. Counting 1 person or 1 million people takes the same amount of RAM (one integer).
- The output is tiny. Instead of writing 10,000 'view' rows to the database, it writes one row: `(minute: 12:05, views: 10,000)`.
The output of the stream processor is written to a fast, specialized analytical database (like Apache Druid, ClickHouse, or Pinot). These databases are optimized for rapid dashboard queries over pre-aggregated data.
The Serving Layer
When the user opens their dashboard to see "Views over the last 30 days":
- The API queries the Data Warehouse for the first 29 days (highly accurate, deeply historical).
- The API queries the Fast OLAP database for the current day (real-time, slightly less accurate).
- The API merges the two results and returns them to the user.
Handling Time
In stream processing, time is the hardest problem. There are two kinds of time:
- Event Time: When the event actually happened on the user's phone.
- Processing Time: When the event arrived at your servers.
If a user goes into a subway tunnel, their phone might record 5 clicks (Event Time: 12:01). They emerge from the tunnel at 12:05, and the phone uploads the 5 clicks (Processing Time: 12:05).
If your stream processor aggregates based on Processing Time, the dashboard will show a spike of activity at 12:05, which is a lie. The analytics will be wrong.
Failure modes
Querying Kafka directly. Kafka is a log, not a database. You cannot query it for "Total views for video X". Attempting to build an API that scans Kafka will fail immediately. Kafka is a pipe; you must pour the pipe into a database to query it.
Dual writes. A common mistake is having the application server try to write to the primary database AND to Kafka simultaneously. If the database write succeeds but the Kafka write fails, the systems are out of sync. The application should write only to the database, and use Change Data Capture (CDC) to stream the changes to Kafka, or write only to Kafka and use a consumer to update the database.
Assuming the Speed Layer is perfectly accurate. The Speed Layer is a best-effort approximation. Messages can be duplicated during network retries, or dropped if they arrive too late. The Batch Layer is the source of truth; it overwrites the Speed Layer's approximations every night. (This is changing with modern streaming systems that guarantee exactly-once processing, but the principle holds for legacy setups).
What to carry forward
- Explain why relational databases fail at massive event-ingestion scales.
- Design a stream processing pipeline that reduces write volume by orders of magnitude.
- Solve the late-arriving data problem using Event Time and watermarks.
- Combine a Batch Layer and a Speed Layer to serve a comprehensive dashboard.