Designing an Ad Click / Impression Counter

A system design interview guide to counting billions of ad impressions and clicks per day accurately enough to bill on, while still surfacing fresh numbers on a dashboard within seconds.

Student edition · start with the guarantee

Count impressions and clicks from a replayable event stream, while distinguishing provisional dashboard numbers from auditable billing.

Correctness contract: One accepted billable event ID contributes at most once to its finalized billing period. Dedup, state, offsets, and corrections have defined retention and transactional boundaries.

Basic HTTP, databases, and programming are assumed. Specialized terms are explained below. Named products motivate the problem; this is an educational design, not a claim about their current internals. Capacity numbers are illustrative decimal payload estimates, not benchmarks. Add indexes, protocol overhead, replicas, headroom, and failure capacity before provisioning.

Counting sounds like the simplest thing a computer can do, and at small scale it is — increment a number in a database and move on. The problem becomes interesting precisely because of scale and money. An ad platform serves an enormous volume of impressions and clicks, and those counts are not a vanity metric: advertisers are billed against them and the platform's revenue is computed from them. That raises the bar from "roughly right" to "defensibly correct," while the sheer write rate makes the naive "increment a row" approach fall over almost immediately. This guide builds up a design that counts at very high volume, deduplicates so a single click is never billed twice, tolerates events that arrive late or out of order, and reconciles itself to an exact answer that you can stand behind in an invoice.

Contents

  1. Requirements and Clarifying Questions
  2. The Scale of Writes
  3. Back-of-the-Envelope Estimation
  4. What We Count
  5. API and Data Model
  6. The Hard Parts
  7. Streaming Aggregation Pipeline
  8. Deduplication by Event ID
  9. Rollups and Pre-Aggregation
  10. Late and Out-of-Order Events
  11. Batch Reconciliation
  12. Billing-Grade Accuracy
  13. Failure Modes
  14. Tradeoffs and Alternatives
  15. A 45-Minute Interview Plan
  16. Common Mistakes
  17. Summary
  18. Check Yourself
  19. Key Terms
  20. Concepts to understand first
  21. Worked example
  22. Runnable Python model

Concepts to understand first

Each definition includes a concrete example and the condition under which the mechanism is useful. You can read this guide without opening another guide.

Deduplication

Recognizing multiple deliveries of one logical event using a stable identity. The same logical event must keep its ID across retries, and claiming the ID must be atomic with the counted or stored effect.

Example. Events e1,e2,e1 should yield a count of 2. An exact seen set plus the increment in one transaction makes the second e1 a no-op.

Limit. A TTL limits the dedup horizon; retries after expiry may count again. Approximate filters may drop new events, and changing the ID or routing a retry to another local seen set defeats dedup.

Bloom filters

A bit array plus several hash functions used for approximate membership. Adding an item sets its hash-selected bits; a query with any unset bit is definitely absent, while all-set means only 'possibly present'.

Example. At a chosen 1% false-positive rate, about 9.6 bits per inserted item are needed near optimal sizing. A new click may be called 'seen' even though it is not a duplicate.

Limit. Never drop exact billable events solely because a Bloom filter says 'present'. Use it to avoid unnecessary exact lookups; overload, deletion, incompatible hashing, or lost state invalidate the usual guarantees.

Offsets and checkpoints

An offset is a position in one log partition. A checkpoint is a recoverable, consistent record of input positions and processing state. On restart, state must correspond exactly to the input that is considered processed.

Example. If a batch increments a total and crashes before saving its offset, replay increments twice. Commit the total, dedup state, and partition position together, or use a compatible transactional/idempotent sink.

Limit. Saving an offset after a write avoids that specific loss window but can still duplicate; framework checkpoints alone do not make arbitrary external effects transactional.

Event time and watermarks

Event time is when the producer says an event happened; processing time is when a consumer handles it. A watermark is the processor's policy-driven estimate of event-time progress, used to emit and eventually clean up window state.

Example. An event for 12:00 arrives at 12:07. A 5-minute allowed-lateness policy may update its window, divert it to correction, or reject it according to the API contract.

Limit. Watermarks are not proof all earlier events arrived. Idle inputs, skew, and unbounded delay require explicit policies; later corrections must propagate to rollups and reports.

Rollups and weighted aggregation

A precomputed summary per time bucket. Sums and counts merge by addition, min/max by comparison. An average is derived as total sum divided by total count, not an unweighted average of bucket averages.

Example. One window has [10], another [20,20,20]. Merge sum=70 and count=4 to get 17.5, not the unweighted mean of window means (10+20)/2=15.

Limit. Do not sum cumulative counter snapshots to compute traffic, or average p99 values to get a global p99. Retain reset-aware counter increases or compatible histogram distributions.

Transactional outbox

A business transaction writes its state change and an event row to the same database. A relay publishes committed event rows later, retrying until the broker durably accepts them.

Example. An order and its OrderCreated outbox row commit together. A relay crash after publish but before marking sent publishes the same event ID again on restart.

Limit. The relay is at-least-once, not magically exactly-once. Consumers dedup transactionally; preserve per-entity sequence when order matters. CDC is change data capture: reading committed database changes.

1. Requirements and Clarifying Questions

Before drawing any boxes, spend the first few minutes pinning down what "count" means here. The answers change the design, so ask out loud.

FunctionalNon-functional
Record every impression and click event.Absorb about 1.7 million events per second at peak without loss.
Query counts and CTR per ad / campaign over any time range.Dashboard freshness under 60 seconds; query latency under a second.
Produce final per-period counts for invoicing.Billing counts exact: no double counting, no loss, fully auditable.
Filter invalid (bot / fraud) traffic from billable counts.Highly available ingest: losing the dashboard is bad, losing events is worse.
Interview tip

Say the tension out loud early: "Dashboards want fresh numbers, billing wants exact numbers, and I can't get both from one path cheaply." That single sentence tells the interviewer you already see where the design is going.

2. The Scale of Writes

The first number to establish in an interview is the write rate, because it dictates almost every later decision. A large ad platform might serve on the order of hundreds of thousands to millions of impressions per second at peak, with clicks a couple of orders of magnitude rarer but still very high in absolute terms. Each of those is a discrete event that the counting system must observe. There is no batching at the source — the events stream in continuously, all day, with diurnal peaks and occasional spikes from large campaigns.

At that rate the naive design — "on every event, run UPDATE counts SET n = n + 1 WHERE ad_id = ?" — is dead on arrival. A single row per ad becomes a contention hotspot; the database cannot sustain millions of write transactions per second; and you would be paying transactional durability costs on every single increment. The realization that drives the architecture is that you do not need to durably persist a final count on every event. You need to durably capture the raw event cheaply, and then aggregate it asynchronously. Capture and counting are separate problems with separate performance profiles.

QuantityRough orderWhy it matters
Impressions / secHundreds of thousands to millionsSets the ingestion throughput the pipeline must absorb without dropping events.
Clicks / secThousands to tens of thousandsRarer but the revenue-critical events; dedup matters most here.
Distinct adsMillions of active creativesThe cardinality of the keyspace you aggregate over (per-ad, per-minute).
Daily eventsTens of billionsDrives storage sizing for raw logs and the cost case for rollups.
A useful framing for an interview: this is not a "database write throughput" problem, it is a stream processing problem. The moment you accept that you cannot synchronously update a final count per event, the design opens up — ingest cheaply, aggregate downstream, and store compact rollups rather than raw rows.

3. Back-of-the-Envelope Estimation

Work the numbers step by step. The goal is not precision; it is to find which parts of the system are big and which are small. Assumptions: 50 billion impressions a day, a 1% click-through rate, peak traffic 3× the average, and about 200 bytes per raw event.

StepWorkingResult
Impressions per second (average)50,000,000,000 ÷ 86,400 s≈ 579,000 /s
Impressions per second (peak)579,000 × 3≈ 1.74 M /s
Clicks per second (peak)1.74 M × 1%≈ 17,000 /s
Raw impression bytes per day50 B events × 200 B10 TB /day
Kafka ingress at peak1.74 M/s × 200 B≈ 350 MB/s
Kafka partitions350 MB/s ÷ ~10 MB/s per consumer, then headroom for hot keys~35 → use 64
Minute rollup rows per day1 M active ads × 1,440 min1.44 B rows
Minute rollup storage1.44 B rows × ~50 B, kept 30 days72 GB/day, 2.2 TB
Click dedup set (1 hour window)500 M clicks/day ÷ 24 × ~40 B per id≈ 0.8 GB
Impression dedup set (1 hour window)2.08 B ids/hour × ~40 B≈ 83 GB (too big!)
Impression dedup with a Bloom filter2.08 B ids × 9.6 bits (1% false positives)≈ 2.5 GB

Two lessons fall out of the table. First, the rollup store is small compared with the raw stream; aggregation does most of the work. Second, exact dedup is affordable for clicks but not for impressions, which is why many systems use an exact seen-set for clicks and a Bloom filter (or no streaming dedup at all, relying on batch) for impressions.

Common mistake

A Bloom filter's false positives make it say "seen" for some events that are new, so those events are dropped. That is an undercount. Fine for a provisional impression dashboard, never acceptable for billable clicks.

4. What We Count

Before designing the plumbing, be precise about the quantities, because their definitions affect both correctness and the data model. Three things are in play, and they are not independent.

The key design consequence is that you count impressions and clicks as two separate event streams keyed the same way (per ad, per time bucket), and you compute CTR on read. That keeps the write path uniform — both are "count this event into this bucket" — and keeps the reported ratio always consistent with the counts it is built from.

5. API and Data Model

Keep the write API tiny and the read API shaped like the queries you agreed on.

# write path (from ad servers / SDKs; batched)
POST /v1/events
  [{ "event_id": "7f3c…", "type": "click", "ad_id": 91842,
     "campaign_id": 5521, "ts": "2026-09-25T14:03:07.412Z",
     "user_hash": "…", "country": "BR", "device": "android" }, …]
  → 202 Accepted          # accepted into Kafka, not yet counted

# read path (dashboards, reports)
GET /v1/ads/91842/counts?from=2026-09-25T00:00Z&to=2026-09-25T12:00Z&granularity=minute
  → { "status": "provisional", "points": [ { "t": "…14:03", "impressions": 8123, "clicks": 97 }, … ] }

GET /v1/billing/campaigns/5521?period=2026-09-24
  → { "status": "final", "billable_clicks": 180442, "invalid_filtered": 3127 }

Note the status field: the API tells the caller whether a number can still change.

TableKeyColumnsNotes
raw_events (Kafka + object storage)event_idall event fieldsAppend only. The source of truth and audit trail.
counts_minute(ad_id, minute)impressions, clicks, is_finalWritten by the stream job, overwritten by reconcile.
counts_hour, counts_day(ad_id, hour) / (ad_id, day)sameRolled up from the finer tier.
billing_counts(campaign_id, period)billable_clicks, invalid, job_run_idImmutable once issued; corrections are new rows.
Why store impressions and clicks in one row?

Both are keyed by (ad_id, minute), and CTR needs both. One row per key means one read computes CTR, and the two counts are always from the same run.

6. The Hard Parts

It is worth naming the difficulties explicitly, because the architecture exists to answer each of them in turn. An interviewer is really probing whether you see past the word "count."

7. Streaming Aggregation Pipeline

The backbone of the design is a streaming pipeline that separates the four concerns — capture, buffer, aggregate, serve. Each stage has one job and can be scaled independently of the others.

Ad counter pipeline: ingest, Kafka partitioned by ad_id, aggregator with dedup and in-memory counts, atomic flush to the rollup store, dashboards
Events (impressions and clicks) are ingested and written to a durable Kafka queue, partitioned by ad id. A stream aggregator skips event ids it has already seen and keeps per-ad, per-minute counts in memory. Each flush adds those counts to the rollup store and records the Kafka offset in the same transaction. Dashboards and APIs query the rollup store for counts and CTR.

Walking the stages from left to right:

function on_event(event):
  validate(event)                          # reject malformed
  kafka.produce(topic=event.type,          # "impression" or "click"
                key=event.ad_id,           # same ad -> same partition
                value=event)               # durable, replayable

function aggregator_loop():
  counts = {}                              # (ad_id, minute) -> n, in memory
  for event in kafka.consume():
    if seen(event.event_id): continue      # dedup, see next section
    bucket = floor_to_minute(event.ts)
    counts[(event.ad_id, bucket)] += 1
    if flush_due():
      # add these partial counts to the stored rows AND record the
      # Kafka offset in the same transaction (or use Flink checkpoints)
      rollup_store.add_and_commit(counts, kafka.position())
      counts.clear()
The crash-after-flush trap

A flush adds partial counts to rows that may already hold counts for the same minute, so it is an add, not an overwrite. Adds are not idempotent. If the consumer adds its counts, then crashes before committing its Kafka offset, it will re-read those events on restart and add them a second time. The fix is to make the count update and the offset commit one atomic step: store the offset in the same database transaction as the counts, or let a framework such as Flink snapshot state and offsets together (a checkpoint) and use a transactional or idempotent sink. The seen-set used for dedup must be part of that same snapshot, or a restart forgets which ids it already counted.

Watch out: hot ads

Hot-ad salting spreads aggregation work, but exact dedup needs stable routing: derive salt from event_id or dedup before salting. Arbitrary retry routing can count the same logical event on multiple shards. Add partial counts only after that identity rule is satisfied.

8. Deduplication by Event ID

Define a logical event ID at the source and preserve it across retries. An accidental double-tap may be two distinct events, not a network duplicate; fraud/invalid-event classification is a separate policy. Namespace IDs by source if different producers can reuse them.

For exact counted effects, the event claim, aggregate update, and relevant input position/state must be transactional or recoverable from one compatible checkpoint. An in-memory seen check alone loses state after a crash. A TTL bounds the guarantee: events replayed after expiry can count again. Dashboard dedup may use a bounded horizon; billing finalization needs retained exact identities or immutable contribution records covering all accepted corrections.

A Bloom filter can be a prefilter for an exact lookup. It cannot by itself discard billable events without undercounting new IDs. Whether an 83 GB provisional seen payload is affordable depends on partitioning and budget, not a universal 'too big' threshold.

Hot-key salting must route retries consistently (for example salt=hash(event_id)%s), or dedup must happen before the salted aggregates. Counts add only after each logical event has been assigned once. Rollups and invoice revisions need versioned corrections so reconciliation does not race streaming writes.

9. Rollups and Pre-Aggregation

Storing one row per raw event would be ruinous at tens of billions of events a day, and querying it for a dashboard would be hopeless. The answer is to pre-aggregate into rollups at increasing granularity, so each query reads the coarsest tier that can answer it.

Minute to hour to day rollup tiers
Per-minute counts are summed into per-hour counts, which are summed into per-day counts. Coarser tiers hold far fewer rows, making long-range queries cheap.

The aggregator produces the finest tier — per ad, per minute. A rollup job then folds minutes into hours and hours into days. The arithmetic is trivial because counts are additive: an hour is just the sum of its sixty minutes. The payoff is in row count and query cost. A "last 30 days" report for an ad reads thirty day-rows instead of forty-three thousand minute-rows. You keep the fine-grained minute tier only for a short recent window (where people zoom in), and let the coarse tiers carry the long history.

TierBuckets per day per adGood for
Minute1440Recent, fine-grained dashboards; live monitoring of a campaign.
Hour24Day-level trends without minute noise; intraday reporting.
Day1Long-range reports, billing summaries, historical retention.

10. Late and Out-of-Order Events

Because events arrive out of order, the aggregator cannot treat a minute bucket as final the instant the wall clock leaves that minute. The standard tool is a watermark with a grace period: a minute bucket stays open and accepting late arrivals for a while after it ends, and only then is its count flushed as provisional. An event stamped with a time inside an already-flushed bucket is not dropped; it is applied as a correction to that bucket.

The watermark is the pipeline's running estimate of "we have now seen everything up to time T". It is usually derived from the event times themselves (for example, the newest event time seen minus a few seconds), not from the wall clock, so a replay of old data behaves the same as live data.

This is why every event carries its own event timestamp from the source, and the aggregator buckets by that timestamp rather than by arrival time. Arrival-time bucketing would scatter an offline client's ten-minute-old events into the wrong minute. Event-time bucketing plus a grace window keeps them in the bucket they belong to, at the cost of a short delay before a bucket's count settles.

function assign_bucket(event):
  bucket = floor_to_minute(event.ts)       # event time, not arrival time
  if watermark() > bucket + BUCKET + GRACE: # watermark = "no older events expected"
    rollup_store.add_correction(event.ad_id, bucket, +1)  # late: fix the closed bucket
  else:
    open_buckets[(event.ad_id, bucket)] += 1               # still within grace

The grace period is a deliberate trade. A longer grace catches more stragglers before flushing, improving the freshly reported number, but delays when a bucket's count appears. A shorter grace makes counts appear sooner but relies more on corrections (and ultimately on reconciliation) to reach the true value. Either way, the streaming counts are explicitly provisional — which sets up the final piece.

11. Batch Reconciliation

The streaming path is fast but approximate: it can miss very late events, mishandle a rare dedup edge case across a window boundary, or carry a small error from a consumer that crashed and resumed. For numbers you will bill on, you need a path that is slow but exact. This is the classic lambda-style arrangement — a real-time layer for freshness alongside a batch layer for correctness.

Lambda vs kappa

Running two code paths (streaming and batch) that must compute the same answer is the main cost of the lambda architecture: every bug has to be fixed twice. The kappa alternative keeps one streaming job and, when exact numbers are needed, replays the retained log through the same job. Modern engines with exactly-once state (Flink, Kafka Streams, Beam/Dataflow) make kappa practical. Billing systems still often keep a separate batch pass because it is the simplest thing to audit.

Events Kafkaraw log, retained Stream aggregatorseconds, windowed dedup Batch reconcilehourly/daily, global dedup Dashboardprovisional, fresh Billingfinal, exact, auditable overwrites closed periods
Two paths read the same retained log. The speed path gives fresh but provisional numbers. The batch path later replaces them with exact counts, and only those are billed.

Because every raw event was durably written to the queue (and archived to long-term storage), you have a complete, replayable record. A periodic batch job — say, hourly or daily — reads the full set of raw events for a closed period, deduplicates globally (no window limitation, since it sees everything at once), aggregates exactly, and writes the authoritative counts. Where the batch result disagrees with the streaming rollup, the batch value wins and the rollup is corrected.

function reconcile(period):
  raw   = event_archive.read(period)       # every raw event for the period
  uniq  = dedup_by_event_id(raw)           # global dedup, no window limit
  exact = group_and_count(uniq, by=[ad_id, minute])
  rollup_store.overwrite(period, exact)    # batch is authoritative
  emit_billing_counts(exact)               # numbers we invoice on
The mental model: the streaming layer answers "what's happening right now?" and the batch layer answers "what exactly happened?" Users see fast provisional numbers that converge to the reconciled truth as each period closes. You are not choosing between fresh and accurate — you are serving both from the same raw event log.

12. Billing-Grade Accuracy

Billing uses finalized, auditable logical events after exact dedup and the applicable validity rules, not a probabilistic provisional dashboard. 'Final' is a business cutoff under a documented accepted-lateness/adjustment policy; an unbounded network cannot prove that every possible old event has arrived.

Keep immutable invoice/count versions, rule versions, source ranges, and reconciliation IDs. Late accepted events or fraud reclassification produce explicit adjustments. Preserve enough raw evidence to explain each billed contribution. CPM impression billing needs exact impression treatment too, not only clicks.

13. Failure Modes

For each box, ask "what if this dies or slows down?" and have an answer ready.

FailureWhat happensMitigation
Ingest collector crashesIn-flight requests fail.Stateless collectors behind a load balancer; clients retry with the same event_id, so retries are safe.
Kafka broker lostPartitions it led become briefly unavailable.Replication factor 3, acks=all, min.insync.replicas=2; a replica takes over.
Aggregator crashes mid-windowIn-memory counts lost.Restart from the last checkpoint (counts + offsets + seen-set together) and replay; no double count.
Aggregator falls behind (lag)Dashboards go stale.Alert on consumer lag; add consumers up to the partition count; salt hot keys.
Rollup store downFlushes fail; dashboard reads fail.Aggregator holds state and retries; Kafka retains events; nothing is lost, only delayed.
Bad deploy miscounts for 3 hoursWrong provisional numbers.Fix the bug, then replay the retained raw log for those hours. This is why raw events are kept.
Click-fraud burstInflated clicks.Fraud filter in the batch pass decides what is billable; streaming numbers are labelled provisional.

14. Tradeoffs and Alternatives

ChoiceOption AOption BPick when
Where to countIncrement a DB row per eventStream aggregate, flush rollupsA only at tiny scale (hundreds of events/s). B otherwise.
Correctness pathLambda: stream + batchKappa: one stream job, replay to fixA when billing needs a simple, auditable batch job; B when one codebase matters more.
Streaming dedupExact seen-setBloom filterA for clicks (money). B for impressions (volume).
Rollup storeTime-series DBColumnar OLAP (Druid, Pinot, ClickHouse)OLAP when you need slice-and-dice by many dimensions; TSDB for simple per-key series.
Grace periodShort (seconds)Long (minutes)Short for fresher dashboards; long for fewer corrections.

Worked example: counting a retried click

Input deliveries are e1,e2,e1; all three are durably stored as deliveries, but only two logical events should contribute.

Worked example, in orderClaim e1 and increment: A unique event claim and its contribution commit together. Do not persist 'seen' separately before a count that may never commit.. Count e2: The provisional count is 2. The source must preserve each logical event's ID across retries.. Replay e1 after a crash: Restore matching count, dedup, and partition-position state. The repeated ID is a no-op within the retained exact dedup horizon.. Finalize with late corrections: A deterministic batch recomputes the period from retained raw logical events and fraud rules. Versioned finalization replaces/adjusts all derived rollups; invoice corrections are new auditable records.1Claim e1 and increment2Count e23Replay e1 after a crash4Finalize with late corrections
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Claim e1 and incrementA unique event claim and its contribution commit together. Do not persist 'seen' separately before a count that may never commit.
2. Count e2The provisional count is 2. The source must preserve each logical event's ID across retries.
3. Replay e1 after a crashRestore matching count, dedup, and partition-position state. The repeated ID is a no-op within the retained exact dedup horizon.
4. Finalize with late correctionsA deterministic batch recomputes the period from retained raw logical events and fraud rules. Versioned finalization replaces/adjusts all derived rollups; invoice corrections are new auditable records.
Check your understanding: Is salting an ad key always safe for exact dedup?

No. Retries must reach the same dedup authority, for example with salt derived from stable event_id, or dedup must be global. Arbitrary salt can count the same event on two shards.

Run a small, testable model

Download concepts.py. It uses only the Python 3.10+ standard library. From the directory where you saved it, run:

python3 concepts.py dedup
python3 concepts.py test

The SQLite model atomically claims event IDs and updates a total, then verifies replay and rollback.

The program contains deterministic assertions and failure traces. It is a learning model, not a production service: it does not implement network consensus, distributed durability, authentication, or third-party delivery. Read its contracts and try changing an input before extending it.

Primary references for these concepts

15. A 45-Minute Interview Plan

MinutesWhat to doWhat to say or draw
0–5ClarifyEvents, freshness, billing exactness, late data, query shapes.
5–10Estimate1.7 M events/s peak; 10 TB/day raw; rollups are small; clicks dedup is cheap.
10–20High-level designIngest → Kafka → stream aggregator → rollup store → query API. Say why the naive DB counter fails.
20–35Deep divesDedup by event_id; event time + watermarks; atomic flush + offset; hot-key salting.
35–42Correctness for moneyBatch reconciliation, fraud filtering, provisional vs final, audit trail.
42–45Wrap upFailure modes, what you'd monitor (lag, dedup rate, stream-vs-batch drift), what you'd build next.

16. Common Mistakes

17. Summary

An ad click and impression counter is a stream-processing system whose every decision is shaped by two pressures that pull apart — enormous write volume and money-grade accuracy.

ConcernMechanism
How do we absorb millions of events per second?Decouple capture from counting: cheap ingest, durable queue, asynchronous aggregation.
What do we actually count?Impressions and clicks as two event streams keyed per ad per minute; CTR derived on read.
How do we count each event once?Source-assigned event id checked against a windowed seen-set before incrementing.
How do we serve dashboards cheaply?Pre-aggregated rollups, minute → hour → day, query the coarsest tier that fits.
How do we handle late and out-of-order events?Bucket by event time with a watermark grace period; apply corrections to closed buckets.
How do we get an exact answer?A lambda-style batch reconciliation job replays the raw log and overwrites with exact counts.
How do we bill safely?Exactly-once on billable events, retained raw log for audit, invalid-traffic filtering, provisional vs final labels.
The recurring theme: capture cheaply and exactly once, aggregate asynchronously into rollups for speed, and reconcile from the durable raw log for truth. Freshness and accuracy stop competing the moment you serve a fast provisional number that converges to a slow exact one.

18. Check Yourself

  1. An ad shows 900 impressions and 9 clicks in minute 1, and 100 impressions and 5 clicks in minute 2. What is the CTR for the two minutes?
    Answer

    14 ÷ 1,000 = 1.4%. Averaging the two minute CTRs (1% and 5%) would wrongly give 3%. This is why you store counts, not ratios.

  2. A consumer adds its partial counts to the store, then crashes before committing its Kafka offset. What goes wrong, and how do you prevent it?
    Answer

    On restart it re-reads the same events and adds them again, so counts are too high. Commit the offset in the same transaction as the counts, or use checkpointed state with a transactional or idempotent sink.

  3. Why do we bucket events by event time instead of arrival time?
    Answer

    Arrival time depends on the network and on when offline clients reconnect. Event time puts each event in the minute it actually happened, so replays and late uploads give the same answer.

  4. Your impression dedup uses a Bloom filter with 1% false positives. Does it overcount or undercount, and by about how much?
    Answer

    It undercounts. About 1% of new events are wrongly judged "seen" and dropped (a bit less while the filter is still filling). That is why Bloom filters are not used for billable clicks.

  5. One ad suddenly gets 20% of all traffic. What breaks, and what is the fix?
    Answer

    Its partition, and the single consumer that owns it, cannot keep up, so lag grows for that ad. Salt the key (for example ad_id#0..7), count each sub-key, and sum at read time.

  6. What is the difference between the numbers on the dashboard and the numbers on the invoice?
    Answer

    Dashboard numbers are provisional: fast, windowed dedup, may still change. Invoice numbers come from the batch pass over the full raw log: global dedup, fraud filtered, for a closed period, and never change.

19. Key Terms

Impression
One showing of an ad to a user.
CTR
Click-through rate: clicks ÷ impressions for the same ad and window.
Event time
Event time is when the producer says an event happened; processing time is when a consumer handles it. A watermark is the processor's policy-driven estimate of event-time progress, used to emit and eventually clean up window state. Example and limits above.
Watermark
Event time is when the producer says an event happened; processing time is when a consumer handles it. A watermark is the processor's policy-driven estimate of event-time progress, used to emit and eventually clean up window state. Example and limits above.
Grace period
Extra time a window stays open to accept late events.
Idempotent
Safe to apply twice: the second application changes nothing.
Consumer lag
How far a consumer's position trails the newest message in a partition.
Key salting
Splitting one hot key into several sub-keys to spread its load.
Lambda architecture
A fast streaming path plus a slow exact batch path over the same data.
Kappa architecture
One streaming path; fix history by replaying the log through it.
Bloom filter
A compact set that can say "maybe seen" wrongly but never misses a real member.

Further reading


⌂