sehaj/learn
System design/11 Counting and analytics

11.01 · concept · 10 min

Counting at scale

Counting 100,000 events a second without melting a database: why one row can't take it, the ladder of fixes from batching to pre-aggregation, deciding what must be exact, and the four kinds of count analytics really needs.

The problem: 8.6 billion clicks a day

At 100,000 redirects a second, Snip records about 8.6 billion clicks a day. Owners want, per link: total clicks, clicks per minute for a chart, unique visitors, top countries and referrers, and a live counter during launches. Counting sounds trivial. At this rate it's one of the hardest write workloads Snip has.

Why the obvious way fails

UPDATE links SET clicks = clicks + 1 WHERE code = $1 per click means 100,000 writes a second to the primary, and a viral link's row takes thousands of updates a second, each waiting for the previous one's row lock (02.06 Async workers and queues). Every fix below reduces either how many writes reach storage or how many go to the same place.

The ladder of fixes

  1. Batch. Clicks go to a log; consumers add up a few thousand at a time and write one update per code (02.06 Async workers and queues). Thousands of writes become one.
  2. Pre-aggregate at the edge. Each app server keeps a small map of code → clicks for the last second and sends one event with counts, so a viral link becomes one event per server per second instead of 50,000 (07.04 Partitions, consumer groups and ordering).
  3. Split the hot counter. If one key's batched updates still collide, keep N rows and sum on read (05.02 Hot partitions and celebrity keys).
  4. Count in memory, persist periodically. A Redis INCRBY per batch is fast enough for a live counter. Copying it to Postgres every few seconds is write-behind (06.02 Cache-aside, write-through, write-behind): fine for a display, risky as the only record.
  5. Store time buckets, not one total. Write (code, minute, clicks) rows rather than updating one ever-growing total. Inserts spread out, charts come for free, and the total is a sum (11.02 Time-window aggregation).

Snip combines them: per-second pre-aggregation in app servers, a clicks topic partitioned by code, consumers that write per-minute buckets with their offsets in the same transaction (07.03 Delivery guarantees and idempotent consumers), and Redis for the live number.

Decide what must be exact

Exactness costs. Decide per count:

CountSnip's useNeeds to be
Clicks on a paid campaign linkBilling, contractsExact, eventually: every click counted once
Live counter during a launchExcitementApproximately right, right now
Unique visitorsReportsWithin about 1%
Top referrers this hourDashboardsThe right top ten, roughly ordered

Exact counts get the durable path (log, idempotent consumer, buckets in the database). Approximate counts can use memory and sketches, small structures that answer within a known error using a tiny fraction of the space.

Four kinds of count

Almost every analytics question is one of four, and each has its own tool:

All four are usually computed over time windows (11.02 Time-window aggregation), and all are derived from raw events that are kept somewhere cheaper (11.06 Raw events vs derived data).

Seen in production

ClickHouse

Docs

Incremental materialized views

A ClickHouse materialized view is a trigger that runs a query on blocks of data as they're inserted into a table, and inserts the result into a second, target table. For aggregations, the view stores partial aggregation states (functions with a State suffix), an AggregatingMergeTree table merges rows with identical keys during background merges, and queries finish the aggregation with the matching Merge functions. Counting becomes many small inserts of partial totals rather than updates to one row.

What we foundAggregate as data arrives: a trigger on each inserted block writes partial aggregates that are merged in the background.

Incremental materialized view (ClickHouse documentation)clickhouse.com ↗
Predict first

Snip stores each link's total clicks in one Postgres column, updated by a consumer in batches. Owners now want a clicks-per-hour chart for the last 30 days. What's the best change?

Commit to an answer before you read on.

Recap

Interview questions

SeniorfreqMust-knowDesign

Design a system to count views on posts at 100,000 events per second.

Report a mistakeEdit this page