Designing an Ad Auction (RTB)

A system design interview guide to building a real-time bidding system that, in the few dozen milliseconds it takes a page to load, asks many advertisers what they will pay for one ad slot, runs an auction, and serves the winning ad.

Student edition · start with the guarantee

Allocate ads under specified pricing, deadlines, and spend/exposure accounting.

Correctness contract: Specify clearing units, reserve, ties, deadline, and budget reservations. Repeated callbacks cannot create duplicate billable events.

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.

Real-time bidding is one of the most latency-sensitive systems you will ever be asked to design. Every time a user opens a page or app with an ad slot, an auction has to happen before that slot can be filled, and it has to finish fast enough that the user never notices. The system fans a single request out to many independent bidders, waits only as long as a strict budget allows, picks a winner, charges the right price, and serves the ad — typically inside a window of about a hundred milliseconds. The interesting engineering is not "show an ad," it is doing a distributed, money-moving auction under a hard deadline, at enormous request volume, while spending each advertiser's budget smoothly and never showing the same person the same ad too many times. This guide builds that design up from the moment a slot opens.

Contents

  1. Requirements & Clarifying Questions
  2. Back-of-Envelope Estimation
  3. API & Data Model
  4. The Request Flow
  5. Auction Mechanics
  6. Multi-Slot Auctions: GSP and VCG
  7. The Latency Budget
  8. Fan-Out and Gather
  9. Budget Pacing
  10. Frequency Capping
  11. Tracking for Billing
  12. Scale and Parallelism
  13. Failure Modes
  14. Tradeoffs & Alternatives
  15. A 45-Minute Interview Walkthrough
  16. Common Mistakes
  17. Summary
  18. Check Yourself
  19. Key Terms
  20. Further Reading
  21. Concepts to understand first
  22. Worked example
  23. 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.

First-price, second-price, and GSP

A pricing rule connects bids to allocation and payment. First-price charges the winning bid. A textbook single-item second-price auction charges the highest competing bid, subject to a reserve and specified tie rule. Generalized second price handles ranked multiple slots.

Example. For single-item bids $5,$4,$2 with no reserve, first-price charges $5 and exact second-price charges $4. With equal-quality slots, GSP charges the next ranked bid per click.

Limit. Truthfulness of Vickrey auctions assumes independent private values and the textbook rule. GSP is not generally truthful. Quality weighting changes pricing; do not blindly charge the next raw bid.

Budget reservations and pacing

Pacing is a feedback policy that adjusts bidding toward a forecast spending curve. A hard budget instead requires accounting for both finalized spend and outstanding reservations before admitting more exposure.

Example. Budget $100, spent $90, outstanding reservations $8 leaves only $2 safely allocatable. Ten concurrent $1 wins cannot all rely on a stale '$10 left' read.

Limit. Delayed impression/click costs and refunds complicate reservations. A 95% cutoff is a heuristic, not a proof. Soft frequency caps and hard financial/legal limits need different contracts.

Token bucket

A counter of available tokens that refills at rate r up to capacity B. An admitted request spends its cost. The bucket permits an initial burst but bounds admissions over duration T by approximately B+rT.

Example. B=10, r=1 token/s admits 10 calls immediately, then about one per second. It can admit 70 calls over 60 seconds, so this is not a hard 60-calls-per-minute rule.

Limit. A distributed hard limit needs atomic, durable shared accounting or safely partitioned budgets. Clamp clock rollback, validate costs, and state whether rejected attempts consume tokens.

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.

Backpressure and load shedding

Backpressure tells an upstream producer that the downstream stage cannot accept more work. Buffering delays that signal; shedding deliberately rejects or drops lower-priority work when a bound is reached.

Example. A sink accepts 100 events/s while input is 150/s. A 1000-event queue fills in 20 seconds. The system must then block, reject, or apply its documented drop policy.

Limit. A finite queue cannot absorb an infinite outage. Prioritized messages still need hard quotas, deadlines, and bounded client queues; otherwise an exemption can exhaust the system.

Wall and monotonic clocks

A wall clock maps to civil/epoch time and can jump because of correction. A monotonic clock measures elapsed time locally without wall-clock jumps, but its origin is not a shared global epoch.

Example. Use an epoch timestamp to label an event and a monotonic deadline to bound how long a process waits for the next usable timestamp.

Limit. Neither establishes causal order across machines. Monotonic readings cannot be compared between hosts or assumed to survive reboot with the same origin.

1. Requirements & Clarifying Questions

Start every design by pinning down scope. For an ad exchange, these questions change the design the most:

FunctionalNon-functional
Accept a bid request for an ad slotAnswer within the deadline (p99 < 100 ms)
Fan out to eligible bidders, collect bidsVery high throughput: hundreds of thousands to millions of requests/s
Pick a winner and a clearing price above the reserveBilling accuracy: never charge for an ad that was not shown
Respect budgets and frequency capsHighly available: a lost auction is lost revenue
Log impressions and clicks for billingFair: no bidder gets an unfair edge from the exchange
Intuition

Picture an auctioneer who must sell one painting in a tenth of a second. They shout the lot to every buyer at once, take whatever paddles go up before the gavel drops, and sell. Buyers who are still thinking when the gavel falls are simply out.

2. Back-of-Envelope Estimation

Assume a large exchange with 500K bid requests/s on average and 1M/s at peak. Each request fans out to about 20 bidders, and a request is about 2 KB of JSON.

StepWorkingResult
Peak outbound calls1M requests/s × 20 bidders20M calls/s
Outbound bandwidth20M × 2 KB = 40 GB/s × 8320 Gbps
Auctions per day500K/s × 86,400 s43.2B/day
Auction log43.2B × 500 B per record21.6 TB/day
Frequency-cap store1B users × 20 campaigns × 50 B~1 TB in RAM
Exchange servers1M/s ÷ 5K auctions/s per box, +50% headroom~300 servers
What the numbers tell you

The fan-out, not the request rate, is the real load. 320 Gbps of outbound traffic is why exchanges send a binary format (protobuf) instead of JSON, compress requests, and shape traffic: they only call bidders likely to bid on this kind of slot. The 1 TB frequency-cap store must be sharded across many in-memory nodes.

3. API & Data Model

Exchanges and bidders talk using the OpenRTB standard. A trimmed request and response look like this:

# exchange -> bidder: POST /bid
{ "id": "auc-81f2", "tmax": 100,                  # ms the bidder has
  "imp": [{ "id": "1", "banner": {"w": 300, "h": 250},
            "bidfloor": 0.50 }],                  # reserve, as CPM
  "site": {...}, "device": {...}, "user": {...} }

# bidder -> exchange: 200 with a bid, or 204 = no bid
{ "id": "auc-81f2",
  "seatbid": [{ "bid": [{ "impid": "1", "price": 1.25, "crid": "cr-9",
                          "adm": "<creative markup>",
                          "nurl": "https://dsp.example/win?p=${AUCTION_PRICE}" }] }] }

The nurl is the win notice. The exchange calls it with the real clearing price filled into the ${AUCTION_PRICE} macro, so the bidder learns what it paid.

DataKeyStoreWhy there
Campaigns: budget, targeting, capcampaign_idSQL, cached in memory on every serverChanges rarely, read on every auction
Spend so far today(campaign_id, day)In-memory counters, flushed to durable storageUpdated constantly, read by pacing
Frequency counts(user_id, campaign_id)Sharded in-memory KV with TTLRead inside the deadline
Auctions, impressions, clicksimpression_idAppend-only log (Kafka), then a warehouseBilling and reporting, off the hot path
Common mistake

Storing prices as floating-point dollars. Rounding errors add up over billions of auctions. Store money as integers in the smallest unit you need, often micros (millionths of a currency unit).

4. The Request Flow

The whole system is organized around a single short-lived event: an ad slot becoming available. A user loads a page, a slot on that page needs an ad, and the publisher's side sends a bid request to your ad exchange. From that instant a clock starts ticking, and everything that follows has to complete before it runs out.

Real-time bidding request flow
An ad slot opens; the exchange builds a bid request and fans it out to many demand-side bidders in parallel. It collects whatever bids arrive inside the latency budget, runs the auction to pick a winner and a clearing price, and serves the winning ad, logging the impression for billing.

Reading the flow left to right:

function handle_bid_request(slot):
  request = build_bid_request(slot)          # context, size, signals
  bids    = fan_out_to_bidders(request)      # parallel, deadline-bounded
  winner  = run_auction(bids)
  if winner is None:
    return NO_AD                             # slot stays empty or fallback
  log.impression(winner, slot, price=winner.clearing_price)
  return winner.creative
A useful reframing for an interview: RTB is a distributed auction on a deadline. Almost every design choice — the timeout, the parallel fan-out, the pacing, the tracking — exists to run a fair, money-moving auction inside a window so small the user can never perceive it.

5. Auction Mechanics

Specify currency, units (CPM/CPC), eligibility, reserve, deterministic ties, and deadline. In a textbook independent-private-value single-item Vickrey auction, the highest eligible bidder pays max(reserve,highest competing bid). Truthful bidding is a dominant strategy under that model. 'Second bid plus a cent' is a tick-rule variant, not exactly the continuous-price theorem.

First-price charges the winning bid and generally invites strategic shading. With one eligible bidder, exact second-price charges the reserve. Validate nonfinite/negative bids and use exact money units. Neither the pricing rule nor this tutorial describes every current exchange's internals.

6. Multi-Slot Auctions: GSP and VCG

When CPC bids rank by bid×predicted CTR, a generalized-second-price threshold uses the next bidder's score divided by the winner's quality (plus a documented tick/floor), not merely the next raw bid. The raw-next-bid rule applies to the equal-quality example.

GSP is not generally truthful: at CTRs 0.10,0.05 and values/bids 5,4,2, A earns 0.10 at truthful bidding but 0.15 by bidding 3.50 and taking slot 2. VCG charges each winner's externality and is truthful under its classical assumptions. VCG revenue is not universally less than GSP; $0.40 versus $0.50 is this example's result, not a general ordering.

7. The Latency Budget

The defining constraint of the whole system is the deadline. The auction is part of the user's page load, so the exchange gives the demand side a fixed, unforgiving window — commonly on the order of 100 milliseconds — to respond. A bidder that answers inside the window is in the auction. A bidder that does not is dropped, full stop.

The 100ms latency budget with a timeout cutoff
Each bidder races against a fixed timeout. Bids that arrive before the cutoff are counted in the auction; a bidder that is even slightly late is simply dropped rather than waited on. The exchange runs the auction on whatever came back in time.

This "drop the slow ones" behavior is not a bug or a degradation — it is the correct design. The exchange cannot hold up the user's page waiting for a straggler, and it cannot let one slow bidder set the pace for everyone. A bidder that is consistently late is, from the auction's point of view, a bidder that does not exist. The practical consequences are worth stating plainly:

Where the 100 ms goes (one example split) bidder think time 60 net 10net 10 slack 10 parse 5auction 5 0 ms100 ms
The headline 100 ms is shared. After parsing, two network trips, the auction itself and a safety margin, a bidder gets about 60 ms to decide. That is why tmax tells the bidder its real budget.
In RTB, latency is not a quality metric you tune later — it is the hard boundary the entire architecture is built around. "Be fast or be excluded" is the rule, and the exchange enforces it by never waiting past the cutoff.

8. Fan-Out and Gather

Because the exchange must consult many bidders but cannot afford to ask them one after another, the requests go out in parallel and the responses are gathered with a single shared deadline. Calling bidders sequentially would stack their latencies and blow the budget after just two or three; calling them concurrently means the total wait is the time of the slowest bidder you are still willing to wait for — which is exactly the timeout.

The pattern is a scatter-gather: dispatch the bid request to every eligible bidder at once, then collect responses until the deadline arrives, then stop. The crucial detail is that the gather step is bounded by the clock, not by the number of bidders. You do not wait for all of them; you wait until time runs out and take whatever has arrived.

function fan_out_to_bidders(request):
  deadline = now() + 100ms
  futures  = [send_async(bidder, request) for bidder in eligible_bidders]
  bids = []
  for f in futures:
    remaining = deadline - now()
    if remaining <= 0:
      break                                  # deadline hit: stop collecting
    try:
      bid = f.result(timeout=remaining)      # never wait past the deadline
      bids.append(bid)
    except Timeout:
      continue                               # drop the slow bidder
  return bids                                # whatever made it back in time

This is also where isolation matters: a single hung bidder must never be able to stall the gather. Per-call timeouts, dropping rather than retrying within an auction, and treating a missing response as simply "did not bid" keep one misbehaving DSP from poisoning the whole auction. There is no time for retries inside a single auction — the deadline is the retry policy.

9. Budget Pacing

Pacing adjusts bids/participation toward a forecast traffic-weighted spend target. It is a feedback policy, not a hard budget proof. Delayed cost reports, concurrent wins, and forecast errors can overshoot.

For a hard ceiling, atomically account for finalized spend plus outstanding reservations. Distributed allocations must be disjoint and safely reclaimed; an expired holder can still return a delayed result. A 95% cutoff is a heuristic unless exposure cost/delay is rigorously bounded. When CPC cost is uncertain, reserve conservatively or document a permitted bounded overspend contract.

10. Frequency Capping

Frequency capping limits measured exposures per user/campaign/window. Define whether an exposure means rendered, viewable, or a callback-confirmed impression; identify the user/session scope and explain privacy/identity resets. A low-latency counter can support a soft cap, but concurrent auctions and delayed reports can overshoot.

For a hard cap, reserve exposure allowance atomically before permitting the effect, account for outstanding reservations, and settle/release them through idempotent event identities. A check before bidding followed by a later increment is racy. Atomic increment only after delayed callbacks is not enough to prevent prior concurrent exposures. Safe allocation/reclaim requires a defined failure/uncertainty horizon; fail-open or lost replicated state weakens the bound.

Fixed calendar and trailing-window caps differ; resetting TTL on each increment implements neither automatically. Store explicit window-start/expiry or timestamped contributions and define exact boundaries. Benchmark the counter path and decide how unavailable identity/state changes behavior rather than pretending every cap is exact.

11. Tracking for Billing

An ad auction moves real money, so the system must record what actually happened with enough fidelity to bill advertisers correctly. The two events that matter most are impressions (the ad was shown) and clicks (the user engaged), and depending on the pricing model the advertiser is charged on one or the other.

Tracking is harder than it sounds because the events fire on the user's device, far from your servers, and arrive as a high-volume firehose. The design has to be durable and resistant to double-counting: a winning bid is logged at serve time, an impression event confirms the ad was actually rendered, and a later click event ties back to the same impression. These flow into a pipeline that aggregates spend per advertiser and reconciles it against budgets.

Billing demands the same discipline as any money system: durable logs, idempotent event handling keyed by a request or impression id, and reconciliation. An auction you cannot account for afterwards is an auction you cannot charge for.

12. Scale and Parallelism

The last thing to reckon with is volume. A large exchange handles enormous numbers of bid requests per second, and each one fans out to many bidders — so the real load is requests multiplied by bidders, all under the same hard deadline. That product is what forces nearly every scaling decision in the system.

The unifying idea is that anything on the auction's critical path must be fast and parallel, and anything that can tolerate a small delay — billing aggregation, reporting, reconciliation — is pushed off that path into asynchronous pipelines. The deadline is sacred; everything else bends around it.

13. Failure Modes

What failsWhat happensHow to handle it
A bidder is slow or downIts bids miss the deadlinePer-call timeout; a circuit breaker stops sending to it; cut its traffic share until it recovers
An exchange server crashes mid-auctionThat auction is lostPublisher shows a fallback ad; servers are stateless, so the load balancer routes around it
Traffic spike beyond capacityQueues grow, deadlines slip for everyoneShed load early: answer "no bid" to some requests instead of being late on all of them
Budget counters lag behind real spendCampaign overspends while updates are in flightGive each server a small slice (lease) of remaining budget; stop bidding near 95% of budget
Frequency store shard downCannot read caps for some usersPick a policy: fail open (serve, risk extra exposure) or fail closed (skip, lose revenue)
Impression pixel fires twiceAdvertiser billed twiceDedup on impression_id in the billing pipeline

14. Tradeoffs & Alternatives

ChoiceOption AOption BUsual pick
Pricing ruleSecond price: easy bidding, opaque priceFirst price: transparent, bidders must shadeFirst price for display today; GSP-style for search
Who to callEvery bidder: max competitionOnly likely bidders: less bandwidthTraffic shaping, since fan-out cost dominates
Budget checkSynchronous, strongly consistentLocal slices, async reconcileLocal slices; a small bounded overspend is fine
Wire formatJSON: easy to debugProtobuf: smaller, faster to parseProtobuf at high volume
Frequency cap on store failureFail openFail closedDepends on whether the cap is a promise or a preference

Worked example: a profitable GSP bid deviation

Two equal-quality slots have click rates 0.10 and 0.05; click values are A=$5, B=$4, C=$2.

Worked example, in orderAllocate truthful bids: A wins first, B second, paying $4 and $2 per click. Revenue is 0.10×4+0.05×2=$0.50 per view.. Compute A's utility: A earns 0.10×(5−4)=$0.10 per view.. A shades to $3.50: B wins first; A wins second at $2, earning 0.05×(5−2)=$0.15 per view.. Interpret the deviation: GSP is not generally truthful. Quality-weighted thresholds and budget enforcement must be specified separately.1Allocate truthful bids2Compute A's utility3A shades to $3.504Interpret the deviation
Read top to bottom. The table below explains the state and guarantee at each step.
StepState, calculation, and reasoning
1. Allocate truthful bidsA wins first, B second, paying $4 and $2 per click. Revenue is 0.10×4+0.05×2=$0.50 per view.
2. Compute A's utilityA earns 0.10×(5−4)=$0.10 per view.
3. A shades to $3.50B wins first; A wins second at $2, earning 0.05×(5−2)=$0.15 per view.
4. Interpret the deviationGSP is not generally truthful. Quality-weighted thresholds and budget enforcement must be specified separately.
Check your understanding: What must a hard budget account for beyond observed spend?

Outstanding reserved/allocated exposure cost. Delayed spend counters and a pacing curve alone can overshoot.

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 auction
python3 concepts.py test

The model computes textbook second price and reproduces the GSP utility counterexample.

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 Walkthrough

MinutesWhat to do
0–5Clarify: exchange or bidder, one slot or many, CPM or CPC, deadline, volume.
5–10Estimate: requests/s × fan-out, bandwidth, log volume. Say out loud that fan-out dominates.
10–20Draw the flow: slot → exchange → parallel bidders → auction → creative → impression log.
20–35Deep dives: the deadline and scatter-gather, the pricing rule and why it changes bidding, pacing, frequency caps.
35–42Failures and tradeoffs: slow bidders, overspend, double-counted impressions.
42–45Recap the design against the requirements, then name what you would build next.
Interview tip

Interviewers love the question "why not wait a little longer for more bids?" Answer with the user: the auction is part of the page load, and a slower page loses more money than one extra bidder brings in.

16. Common Mistakes

17. Summary

An ad auction is a distributed, money-moving auction that has to finish inside a window the user can never perceive. Its design is a set of decisions that all serve that deadline:

ConcernMechanism
What kicks off an auction?An ad slot opening triggers a bid request describing the opportunity.
How do we get prices from the market?Fan the request out to many demand-side bidders in parallel.
Who wins and what do they pay?An auction: first-price (pay your bid) or second-price (pay just over the runner-up), above a reserve.
How do we stay within the deadline?A hard latency budget (~100ms); bidders that respond late are dropped, never waited on.
How do we ask everyone at once?Scatter-gather fan-out bounded by the clock, not by the bidder count.
How do we keep budgets from burning out early?Budget pacing: a control loop that smooths spend across the day.
How do we avoid annoying users?Frequency capping via fast per-user, per-campaign counters.
How do we charge advertisers?Durable, idempotent impression and click tracking feeding a billing pipeline.
How do we handle the volume?Parallel fan-out, stateless horizontal scale-out, in-memory hot data, async billing.
The recurring theme: the latency budget governs everything. Bidders are called in parallel and dropped when late, hot state lives in memory, billing is pushed off the critical path, and pacing and capping keep the market healthy — all so a fair auction can run and an ad can be priced and served before the page finishes loading.

18. Check Yourself

  1. In a single-slot second-price auction, why is bidding your true value the best strategy?
    Answer

    Your bid only decides whether you win, not what you pay (the runner-up sets the price). Bidding higher only adds wins where the price is above your value, which lose money. Bidding lower only loses auctions you would have won at a profit.

  2. Bids are $3.00, $2.40 and $1.10, and the reserve is $2.50. What does the winner pay under first price, and under the page's second-price code?
    Answer

    Only $3.00 clears the reserve. First price: $3.00. Second price: there is no eligible runner-up, so the price falls to the reserve, $2.50.

  3. At 1M requests/s, 20 bidders each and 2 KB per request, what is the outbound bandwidth? Name two ways to cut it.
    Answer

    20M calls/s × 2 KB = 40 GB/s = 320 Gbps. Cut it with traffic shaping (only call bidders likely to bid) and a compact wire format such as protobuf with compression.

  4. A campaign has a $2,400 daily budget. A quarter of the day's expected traffic has passed and it has spent $900. What should pacing do?
    Answer

    Target spend is 2,400 × 0.25 = $600. Actual spend of $900 is 1.5× the target, so pacing should throttle down, by lowering bids or entering fewer auctions.

  5. Why can the exchange not simply retry a bidder that timed out?
    Answer

    The deadline has already passed for that auction. A retry would make the page wait, and the auction must run with whatever arrived in time.

  6. Using the GSP example on this page, show that bidder A gains by lying.
    Answer

    Truthful: A wins slot 1, pays $4 and earns 0.10 × (5 − 4) = $0.10. If A bids $3.50, it takes slot 2, pays $2 and earns 0.05 × (5 − 2) = $0.15, which is more.

19. Key Terms

RTB
Real-time bidding: an auction run for each ad impression as the page loads.
Exchange / SSP
Runs auctions for publishers' ad slots.
DSP
Demand-side platform: bids on behalf of advertisers.
Reserve (floor)
The lowest price the publisher will accept.
Clearing price
What the winner actually pays.
Bid shading
Bidding below your value in a first-price auction to avoid overpaying.
GSP
A pricing rule connects bids to allocation and payment. First-price charges the winning bid. A textbook single-item second-price auction charges the highest competing bid, subject to a reserve and specified tie rule. Generalized second price handles ranked multiple slots. Example and limits above.
VCG
Winners pay the harm they cause others. Truthful.
eCPM
Expected revenue per thousand impressions; for CPC bids, bid × pCTR × 1000.
CPM / CPC
Price per thousand impressions / price per click.
Pacing
Spreading a budget across the day.
Frequency cap
Limit on how often one user sees one campaign.
Win notice
Callback telling the winning bidder the clearing price.

20. Further Reading


⌂