Design an ad serving system
Design the system that picks which ad to show when a page or app has a slot to fill: find the campaigns allowed to show to this person here, predict how likely each one is to be clicked, run an auction, respect every advertiser's budget and frequency limits, and answer in tens of milliseconds, half a million times a second. Counting the clicks afterwards is D17; this is the decision before.
1. The prompt
"Design the ad serving system for a large app: advertisers create campaigns with targeting and budgets; when a user opens a page with an ad slot, choose the ad."
What makes it hard:
- A hard deadline. The page won't wait: if we haven't answered in about 100 ms, the slot is filled with something else and the money is lost.
- Money moves on every decision. Budgets must not be overspent by much, and advertisers are charged for what they were promised, no more.
- The best ad depends on a prediction. Who pays most per click doesn't matter if nobody clicks; ranking needs a model running inside the deadline.
2. Frame the problem
Whose ads, whose slots?
Our own app's slots (feed and search results), filled by advertisers who buy through our self-serve tools. No external exchanges for now.
A first-party ad network: we run the auction and know the users.
How do advertisers pay?
Mostly per click (CPC) with a maximum bid; some per thousand impressions (CPM).
To compare them we convert everything to expected value per impression (eCPM).
Scale?
500,000 ad requests a second at peak; 5 million active ads in 1 million campaigns; 10 million campaign changes a day.
Too many ads to score them all per request: we need retrieval first, then ranking.
Latency?
The ad server must answer within 50 ms at p99 (the rest of the 100 ms goes to the network and rendering).
Every stage gets a budget, and slow stages are cut off, not waited for.
Budget and pacing?
Daily budgets per campaign, spent evenly through the day; overspend under 1–2%.
Spend must be tracked in near real time across hundreds of ad servers.
Frequency caps?
Yes: e.g. "show this ad to a person at most 3 times a day".
Needs a per-user, per-campaign counter read on every request.
3. What it must do
- Choose an ad for a slot: only eligible ads (targeting, budget, frequency cap, policy), ranked by expected value, chosen by auction.
- Charge correctly: decide the price the winner pays, and record impressions and clicks for billing.
- Pace budgets so campaigns spend through the whole day, never far beyond their budget.
- Apply campaign changes (new ads, paused campaigns, new bids) within a minute.
Not now: the advertiser UI and reporting, creative review, external exchanges (real-time bidding with other networks), click fraud detection (part of D17).
4. How well it must do it
| Target | Why this number |
|---|---|
| Decision within 50 ms at p99 speed | A late answer is worth nothing. |
| Overspend under 2% of daily budget money | Overspend is usually not billed, so it's revenue given away and an unhappy advertiser. |
| Campaign changes live within 60 s; a pause stops spend within 60 s freshness | Advertisers expect "pause" to mean pause. |
| Availability 99.95% surviving failures | Every failed request is lost revenue; fall back to a house ad rather than an error. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Ads to consider | 5 M ads; scoring one with the model takes ~10 µs, so all of them would take 50 s | → so retrieval narrows to ~1,000 eligible candidates with an index, a light model trims to ~100, and the heavy model scores only those |
| Requests | 500,000/s × 100 heavy-model scores = 50 M predictions a second | → so model serving is its own fleet (batched, often on GPUs), and the candidate count is a budget, not a constant |
| Spend updates | 500,000 impressions/s, each changing one campaign's spend | A single counter per campaign would be hot → so each ad server spends from a local allocation and reports in batches |
| Frequency counters | 400 M users × a few active campaigns each | → so caps live in a low-latency key-value store keyed by user, read once per request for all campaigns |
Illustrative numbers.
6. The nouns
- Campaign: budget, schedule, targeting, bid strategy. Ad: the creative (image, text, link) inside a campaign.
- Ad request: a slot, a user (or an anonymous context), a page, a time.
- Candidate: an eligible ad for this request, with its bid and predicted click-through rate (pCTR).
- Impression and click: billable events, each tied to the auction that produced it.
campaign id, advertiser_id, daily_budget, schedule, targeting {age, country, interests[], placements[]}, bid {type: cpc|cpm, max}
ad id, campaign_id, creative_url, status
targeting idx attribute value → posting list of ad ids (in memory on every ad server)
spend campaign_id → spent_today, allocation per ad server (pacing service)
freq (user_id) → {campaign_id: shows_today} (key-value store, TTL 1 day)
auction log auction_id, request, candidates, scores, winner, price (stream → data lake)
7. The contract
Internal: the app's backend calls
POST /v1/ads:select {slot_id, user_id, context {page, country, device}, count: 1}
→ {ad_id, creative, auction_id, impression_url, click_url} or 204 (no ad: show house content)
Tracking (from the client)
GET /i/{auction_id} impression rendered → recorded once (deduplicated by auction_id)
GET /c/{auction_id} click → redirect to the advertiser's link
Advertiser API (separate system) campaigns, ads, budgets, bids → change events
The auction_id ties every impression and click back to the decision, the price and the model version, which is what billing, reporting and training all need.
8. First working version
A fleet of stateless ad servers, each holding the whole targeting index in memory, calling a model-serving fleet for predictions and a key-value store for frequency counts. A pacing service hands out spend. Every decision is logged to a stream.
8.1 One ad request, step by step
8.2 The auction
For each candidate, eCPM = bid per click × pCTR × 1,000 (for CPM bids, the bid itself). The highest eCPM wins. A CPC bid of £2.00 with a 1% predicted click rate is worth £20 per thousand impressions; a £0.50 bid with 5% is worth £25 and wins.
What the winner pays depends on the auction type. In a second-price auction, they pay just enough to have beaten the runner-up, which makes bidding your true value the best strategy. In a first-price auction they pay their own bid, and advertisers' tools then shade bids down. Many display exchanges moved to first-price around 2019; a single-owner network like ours can choose either, and second-price (with a quality-adjusted rank) is the classic choice for self-serve CPC ads. Try both in the lab in section 13.
9. Make it hold up
9.1 Retrieval: 5 million ads to 1,000 speed
| Approach | Why | |
|---|---|---|
| ✗ Bad | Check every ad's targeting against the request | 5 M rule checks per request: far too slow. |
| ~ Good | A database query on targeting columns | Correct, but a database round trip with complex boolean conditions at 500,000 a second. |
| ✓ Great | An in-memory inverted index on every ad server: attribute value → ads that target it (and ads that target "anyone"), intersected for the request's attributes; plus a learned retrieval step (user and ad embeddings, nearest neighbours) for interest-based ads | Boolean targeting becomes posting-list intersections in microseconds (the same structure as a search engine, Module 12). Embedding retrieval finds relevant ads that rules alone wouldn't. |
9.2 The prediction budget speed · quality
9.3 Budget pacing money
Pacing has two parts. A plan: how much the campaign should have spent by now, following the day's traffic curve. A throttle: if spend is ahead of plan, the campaign takes part in fewer auctions (a participation probability), or bids lower; if behind, more. The pacing service recomputes each campaign's throttle every few seconds from reported spend and pushes it to the ad servers.
Overspend comes from delay: impressions already decided but not yet reported. Each ad server spends from a small local allocation of the remaining budget and asks for more as it runs low, so the total in flight is bounded. Near the end of a budget, allocations shrink. Clicks can arrive minutes after the impression, so CPC campaigns keep a reserve for clicks still to come.
9.4 Frequency caps speed · correctness
- One key-value read per request returns the user's counts for all campaigns (a single small document, not one read per ad).
- Increment on the impression (the ad was actually shown), not on the decision; many decisions never render.
- Caps are soft: a few milliseconds of race between two requests from one user can exceed a cap by one. That's acceptable; a strict cap would need a lock per user per request.
- If the store is slow, skip capped campaigns (fail closed) rather than wait past the deadline.
9.5 Campaign changes in under a minute freshness
The advertiser system publishes change events to a stream. Every ad server applies them to its in-memory index (adding and removing ad ids from posting lists) and, every hour or so, loads a full fresh snapshot to correct any drift. A pause is the most urgent change: it also goes straight to the pacing service, which sets the campaign's throttle to zero, a much smaller and faster update than an index change.
9.6 Counting money correctly money
- Every impression and click carries its
auction_id; billing counts each at most once (deduplicate by id), and only those that pass fraud filtering (D17). - The auction log records the price at decision time, so billing doesn't depend on recomputing an auction.
- Real-time spend (for pacing) and billed spend (for invoices) are separate: pacing is fast and approximate; billing is exact, from the deduplicated log, a little later.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
The model-serving fleet is slow.
The ad server has a deadline per stage. If heavy predictions don't come back in time, rank by the light model's scores (or cached pCTR per ad) rather than missing the slot. Watch revenue per request to catch silent degradation.
A new ad has no click history.
The model predicts from the ad's features and its advertiser's history, and a small exploration budget shows new ads a little more often until there's data. Without that, new ads would never win and never learn.
An advertiser says they were charged for 10% more clicks than their site saw.
Compare by auction_id: some clicks are filtered as invalid and not billed; others never reach their site (users go back before the page loads). The auction and click logs make the dispute answerable.
How do you show ads to logged-out users?
Contextual targeting: the page, the search query, country and device. Interest targeting needs consent and an identity, so it's off for them.
Two ads from the same advertiser in one page?
The page asks for several slots in one request; the auction picks a set with rules (at most one per advertiser, spacing), not independent winners.
Ten times the traffic.
Ad servers and model servers scale out; the index is copied to each. Pacing state grows with campaigns, not traffic. The model fleet is the cost to watch: reduce candidates per request before adding hardware.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Model fleet slow | Less relevant ads | Stage timeouts, revenue per request | Fall back to light model or cached pCTR |
| Pacing service down | Nothing visible; spend drifts | Allocation refresh errors | Ad servers stop after their local allocation is used |
| Frequency store slow | Fewer capped ads shown | Read latency | Skip capped campaigns within the deadline |
| Change stream behind | Paused ads still showing | Event lag per server | Pause also goes via pacing (throttle 0) |
| Bad model version | Revenue or click rate drops | Metrics per model version | Canary rollout; fast rollback |
| Whole ad system down | House ads or empty slots | Error rate | The app never waits: fill with its own content |
12. Wrap-up and the clock
Trade-offs we made:
- A funnel: index retrieval, a light model, then a heavy model on a small set, each stage with a deadline.
- eCPM ranking and a second-price auction for self-serve CPC ads.
- Pacing with local allocations: approximate in real time, bounded overspend, exact billing later.
- Soft frequency caps; fail closed when unsure.
- Every decision logged with an auction id, for billing, reporting and training.
What we'd watch: latency by stage · fill rate (requests that got an ad) · revenue per thousand requests · overspend per campaign · time from pause to zero spend · model calibration (predicted vs actual click rate).
At 10×: stateless parts scale out; the model fleet dominates cost, so candidate counts and model size are tuned against revenue.
A 60-minute interview
| 0–5 | Frame: pricing models, scale, deadline, budgets |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns and contract |
| 15–25 | First version: request path and auction |
| 25–50 | Deep dives: retrieval (9.1), prediction budget (9.2), pacing (9.3) |
| 50–60 | Frequency caps or money, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including auction type |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, contract |
| 22–35 | First working version and the auction |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: cold start, disputes, slow models |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Targeting filter, rank by bid × click rate, record impressions and clicks, a budget check. |
| Senior | Inverted-index retrieval, a model funnel inside a deadline, eCPM and auction pricing, pacing with bounded overspend, frequency caps, auction ids for billing. |
| Staff+ | Auction design and its incentives, exploration for new ads, calibration, real-time vs billed spend, graceful degradation that protects revenue. |
Common mistakes:
- Ranking by bid alone.
- Scoring every ad with the full model.
- A central spend counter updated on every impression.
- Checking budgets only once a day, or not at all on the request path.
- Counting frequency on the decision instead of the impression.
- Waiting for a slow dependency past the deadline.
13. Try it, check yourself, read more
The auction lab. Three advertisers want a slot in the feed. Change their bids and see who wins and what they pay.
Predicted click rates are fixed for this user and slot: garden shop 1.0%, bike brand 5.0%, bank 0.6%. Second price: the winner pays the lowest bid that would still have won, plus £0.01.
Which ad should win a slot?
How do you find the ads whose targeting matches a request, among 5 million?
How do you stop overspending without a hot central counter?
When should a frequency-cap counter be incremented?
| Resource | Why |
|---|---|
| Ad click prediction: a view from the trenches (Google, KDD 2013) | Click prediction at scale, calibration and the systems around it. |
| Practical lessons from predicting clicks on ads at Facebook (ADKDD 2014) | Features, freshness and model choice for ad ranking. |
| Generalized second-price auction | The auction family used for many search and feed ads, and its incentives. |