The 100 problems, solved / ML and AI systems / 093 · Design an ad-serving system

Design an ad-serving system

Pick the best ad for a search in 50 ms: targeting, auction, budgets and pacing.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional.

  • When a search results page is built, choose which ads to show for the query and the user, ordered, within the page's latency budget.
  • Advertisers define campaigns: keywords, targeting (location, device, language), bids, and a daily budget that must not be overspent.
  • Selection runs an auction: the ad's rank depends on bid and predicted click-through rate; the winner pays a price set by the runner-up (second-price style).
  • Clicks are recorded for billing and budgets are charged in near real time.
  • Out of scope: the advertiser UI, invoicing, and the click-fraud model beyond consuming its signals.

Non-functional.

  • 100,000 ad requests per second at peak; 10 million active campaigns with 1 billion keyword entries.
  • Ad selection within 50 ms at p99, inside a 200 ms page budget.
  • Budget overspend under 1 percent per campaign per day; underspend from overly cautious pacing also hurts revenue.
  • Availability 99.99 percent; a page without ads is lost revenue, a page that waits for ads is a lost user.
Back of the envelope

Keyword index: 1 billion entries × 50 bytes = 50 GB, sharded in memory across 20 machines or replicated whole on big ones; a lookup for a query's terms returns candidate ads in under a millisecond.

Scoring: 100,000 req/s × 500 candidates = 50 million CTR predictions per second; at 10 microseconds each on a CPU that is 500 cores, or a few dozen GPUs with batching

Budget updates: 100,000 wins per second, each a budget decrement, is too many for one database row per campaign; counters are kept in memory and reconciled, which is the pacing deep dive. Click volume for billing is about 2,000 per second (2 percent CTR).

2. Core entities

  • Campaign: id, advertiser, daily budget, spend today, pacing rate, state.
  • Ad group / creative: campaign, keywords with match types, bid, targeting predicates, creative text, landing URL, quality score.
  • Ad request: request_id, query terms, user context (location, device, language, consented signals), page slots.
  • Auction result: request_id, ranked ads with scores, clearing prices, served_at.
  • Click / spend event: click_id, request_id, ad, price, verdict, time.

3. API

  • POST /ads/select {query, context, slots, deadline_ms} → {ads: [{creative, clearing_price_token}], request_id}. Called by the search front end with a strict deadline; an empty list is a valid answer.
  • GET /click?token= → records the click and redirects to the landing page; the token encodes request_id, ad and price, signed, so the click path needs no lookup.
  • PUT /campaigns/{id} and PUT /adgroups/{id}: advertiser changes, propagated to the serving index within a minute.
  • Internal: GET /budgets/{campaign} → remaining and pacing allowance; POST /budgets/charge {campaign, amount, click_id} idempotent.

4. High-level design

Front endSelectionModels and budgetsData50 msbatch scoreeligibilityimpressionsredirectchargereconcileSearch front endAd selector(retrieve, filter, auction)Keyword index(in memory, sharded)CTR model serversBudget and pacingservice (in-memory counters)Click serviceImpressions and clicksSpend ledger(exact, per campaign)Index builderand model training

One search, one ad:

  1. The front end sends the query and context with a 50 ms deadline.
  2. The selector looks up the query terms (and expansions) in the in-memory keyword index and gets about 2,000 candidate ad groups; targeting predicates and the budget service's eligibility bitmap (campaigns with money left and pacing allowance) cut that to about 500.
  3. The CTR model scores the 500 in one batched call (about 15 ms): predicted click probability given query, ad and context.
  4. The auction ranks by bid × predicted CTR × quality, picks the slot winners, and computes each winner's price as the minimum bid that would have kept its rank; prices are signed into click tokens.
  5. Impressions are logged; when a click arrives, the click service verifies the token, logs the click, charges the budget counter immediately, and redirects. The exact spend ledger is built from the log and reconciles the counters every minute.

5. Deep dives

How does the whole thing fit in 50 ms?

By making every stage a lookup or a batch, and by never touching a database on the request path. The keyword index is rebuilt from the campaign store every few minutes and loaded into memory as a sorted structure, so retrieval is microseconds. Targeting is evaluated as precomputed bitsets per predicate value intersected with the candidate set. The CTR model is the only expensive step, so candidates are capped and scored in one batched call with a 20 ms deadline; if the model is late, a cheaper fallback (historical CTR per ad) scores instead, so the page still gets ads. The pipeline shape is identical to a recommendation funnel, and saying so earns credit: retrieve cheaply, filter, score expensively on few, rank with a formula.

How are budgets not overspent at 100,000 wins per second, without a database lock?

Budget is enforced in two layers. The pacing service keeps per-campaign counters in memory, sharded by campaign id and replicated, and the selector reads an eligibility bitmap from it every second (not per request), so a campaign that is out of money stops appearing within a second, bounding overspend to one second of its spend rate. The exact ledger is computed from the click log, which is the billing truth; counters are reconciled to it every minute, so drift from lost updates is corrected. Charges are idempotent by click_id, which matters because the click service retries. Pacing is the second half: spending a daily budget by 9 a.m. is bad for the advertiser, so each campaign gets a spend curve across the day proportional to expected traffic and the selector throttles a campaign that is ahead of its curve by entering it into fewer auctions (probabilistic participation), which also smooths the counter traffic.

Budget mechanismLatency to stop spendingAccuracyCost per request
Database decrement per winImmediateExactA locked row at 100k/s: not feasible
In-memory counters + 1 s eligibility bitmapAbout 1 sWithin 1 s of spendZero on the request path
Counters reconciled to the click ledgerMinutes for driftExact over a minuteBatch

Why second price, and how is the CTR model kept honest?

A second-price (or generalised second-price) rule makes bidding one's true value the sensible strategy, so advertisers do not shade bids and the platform's inventory is priced by competition, not by guesswork; ranking by bid × CTR means a relevant low bid can beat an irrelevant high one, which protects the user experience and the long-term click rate. The CTR model's calibration matters more than its ranking quality here because prices depend on predicted probabilities: if it overpredicts by 20 percent the auction charges the wrong amounts. So calibration is monitored per segment, the model is trained on logged impressions with position features and inverse-propensity weighting, and its predictions are recalibrated online against observed clicks. Click-fraud verdicts arriving later remove clicks from the ledger before invoicing, which is why the counters are advisory and the ledger is final.

How do campaign edits reach serving within a minute?

Advertiser writes go to the campaign store; a change stream feeds an index updater that applies deltas to the in-memory index on each serving replica (add or remove keyword entries, update bids) and a full rebuild runs every few hours to keep the structure compact. Budget changes go straight to the pacing service. A paused campaign is removed from eligibility within a second and from the index within a minute, and the selector checks eligibility last, so a stale index entry cannot serve a paused ad.

6. What a strong answer sounds like

  • Set the 50 ms budget and allocate it: retrieval under 2 ms, filter under 5 ms, scoring under 20 ms, auction under 2 ms, headroom for the rest.
  • Keep budgets out of the request path with counters and an eligibility bitmap, and name the ledger as the truth that reconciles them.
  • Explain the auction rule in one sentence and why calibration of the CTR model matters for pricing.
  • Describe pacing as spending along a curve, enforced by participation probability.
  • Expected follow-ups: privacy constraints on user signals; how to handle a query the index has never seen (semantic retrieval as a second candidate source); what happens when the model service is down (fallback scores, never an empty page by default).
  • Common mistake: a transactional budget decrement per auction, or scoring every matching ad.

02Where this question was reported