System Design #13: Design a Real-Time Bidding System

SYSTEM DESIGN #13 · INTERVIEW GUIDE

Design a Real-Time Bidding System

Every time you see a banner ad, an auction lasting under 80 milliseconds has already been won — a bid request flew from the publisher’s SSP to dozens of DSPs, bids came back, a winner was selected, and the creative was served. This Real-Time Bidding system is one of the most latency-sensitive distributed systems ever built: it must evaluate a bid request against millions of targeting segments, check budget pacing, apply frequency caps, and return a bid price in under 10ms of internal processing time. The architecture covers IVT fraud filtering, a Redis feature store for sub-millisecond segment lookups, a token-bucket budget pacer, and a second-price auction engine with win/loss notification handling. You will learn how companies like The Trade Desk and DV360 win billions of auctions per day without blowing their clients’ budgets.

OpenRTBRedisToken BucketLightGBMKafka

💡

The Gist — What Problem Are We Solving?

A lightning-fast silent auction happening 500,000 times every second

Every time a webpage loads with an ad slot, a complete auction happens: hundreds of advertisers simultaneously submit bids in under 100 milliseconds. The highest qualified bidder wins and their ad appears — all before the page finishes loading. Miss the 100ms deadline and your bid is discarded. The challenge is doing this at 500,000 auctions per second, pricing accurately, and never overspending a campaign’s daily budget.

💬

Think of it as a stock exchange for ad placements — except it runs 500,000 times per second and each auction lasts less time than a single heartbeat.

Functional Requirements

These are the capabilities the system must deliver — what users and operators can actually do with it.


Bid Processing

⚡Receive OpenRTB bid requests; return valid bid in <100ms

💰
Budget Pacing

💰Real-time spend tracking; smooth daily budget across the day

🏆
Auction Logic

🏆Second-price (Vickrey) auction; floor price; frequency cap enforcement

🤖
Bid Model

🤖ML model predicting pCTR/pCVR per impression; bid = pCTR × value per click

📊
Win/Loss Tracking

📊Record auction outcomes; feed back to pacing and model retraining

Non-Functional Requirements

These define how well the system must perform — the quality attributes that separate a toy from a production system.

⏱️ Latency SLA

⏱️<100ms hard deadline; no bid returned after = bid discarded

📈 Throughput

📈500K bid requests/sec

💰 Budget Accuracy

💰Never overspend by more than 2% of daily budget

🛡️ Fraud

🛡️Pre-bid IVT filter reduces invalid traffic before bidding

🔄 Model Freshness

🔄pCTR scores refreshed every 15 minutes

📊

Key Metrics — The Numbers That Define This System

The headline numbers to know cold — and be ready to explain how each one is achieved.

100ms
hard SLA
500K/sec
throughput
<5ms
bid model
<1ms
budget check
15 min
score refresh
🏗️

System Architecture Diagram

Full data flow from source to serving. Each layer scales independently.

Ingestion
RTB Flow
Publisher

SSP

Ad Exchange

DSP Bidder
User Lookup + ML Score

Processing
Bid Price)

Auction Engine

Win Notice

Creative Delivery; Win/Loss

Storage
Kafka

Pacing + Model Retrain

🗺️

End-to-End User Journey

Trace a single request end-to-end — the story interviewers want you to tell fluently.

1
User loads webpage

— Publisher ad slot fires bid request to SSP; SSP broadcasts to all eligible DSPs simultaneously

2
DSP receives bid request

— Deserialise OpenRTB JSON; extract user_id, placement context, floor price; <5ms so far

3
User lookup

— Redis GET: user_id → segment vector + pCTR score; cache hit in <1ms; in-process memory if available

4
Bid calculated

— bid_price = pCTR × value_per_click × quality_multiplier; check floor price; check frequency cap

5
Budget check

— Token bucket DECR in Redis: bid_tokens -= estimated_cost; if bucket empty → no-bid

6
Bid returned

— OpenRTB bid response returned to SSP; total time <80ms; SSP runs auction among all DSP responses

🔭

High-Level Design — Component Breakdown

Core components — each with a single, well-defined responsibility. The key architectural insight: each layer scales independently, and failure in one component is isolated from the rest.

1
SSP/Exchange
2
DSP Bidder
3
Feature Store
4
Budget Pacer
5
Auction Engine
6
Win Handler
1 — SSP/Exchange

Handles responsibilities for the SSP/Exchange layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

2 — DSP Bidder

Handles responsibilities for the DSP Bidder layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

3 — Feature Store

Handles responsibilities for the Feature Store layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

4 — Budget Pacer

Handles responsibilities for the Budget Pacer layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

5 — Auction Engine

Handles responsibilities for the Auction Engine layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

6 — Win Handler

Handles responsibilities for the Win Handler layer. Designed for independent horizontal scaling — additional instances added without architectural changes. Communicates asynchronously with adjacent components to maximise throughput and fault isolation.

🔬

Low-Level Design — Deep Dives

Deep dives worth explaining in detail in any senior engineering interview. For each: know the data structure, the algorithm, the why, and the trade-off you made.

1 — Bid Request Handler
OpenRTB 2.6 · <5ms parse

Receives OpenRTB 2.6 JSON bid requests from SSP partners via HTTPS. Validates required fields (imp[], site/app, user), applies pre-bid IVT check (IP blocklist, UA bot pattern), and routes to the bidding pipeline. Invalid requests returned as no-bid (HTTP 204) immediately. Request deserialization uses Protocol Buffers for 40% less CPU vs JSON. Each request tagged with a deadline timestamp; any component exceeding deadline returns no-bid immediately.

def handle_bid_request(raw_bytes):
req = openrtb_pb2.BidRequest()
req.ParseFromString(raw_bytes)
if is_ivt(req): return NO_BID
deadline = time.monotonic() + 0.075 # 75ms
return pipeline.process(req, deadline)

2 — Feature Store Lookup
Redis · <8ms · Pre-computed

User and contextual features pre-computed hourly and stored in Redis hashes: user:{id} → {segment_ids[], frequency_cap_counts{}, predicted_ctr}. Site features: site:{domain} → {category, viewability_score, brand_safety_tier}. Lookup uses Redis pipelining — all feature keys fetched in a single round-trip. Cache miss (new user): use prior distribution defaults. Feature freshness: 1-hour stale acceptable for targeting; frequency caps updated in real time.

def lookup_features(user_id, site_domain):
pipe = redis.pipeline()
pipe.hgetall(f’user:{user_id}’)
pipe.hgetall(f’site:{site_domain}’)
user_feats, site_feats = pipe.execute()
return merge_features(user_feats or DEFAULTS, site_feats or {})

3 — Budget Pacer
Token Bucket · Per-campaign

Per-campaign token bucket in Redis. Bucket state: {tokens_remaining, last_refill_ts}. Refill rate = hourly_budget / 3600 tokens/sec. Lua atomic check-and-deduct: if tokens < bid_floor, return no-bid. Deduct bid_amount on win notification. Smooth pacing: if spending 15% ahead of expected linear pace, halve the deduction rate. Distributed bidder nodes share bucket state via Redis — Lua script prevents race conditions.

— Lua: atomic budget check
local budget = tonumber(redis.call(‘HGET’, key, ‘tokens’))
if budget < tonumber(ARGV[1]) then return 0 end redis.call('HINCRBYFLOAT', key, 'tokens', -tonumber(ARGV[1])) return 1

4 — Auction Engine
2nd Price · Bid Shading

Second-price auction: winner pays max(second_highest_bid, floor_price) + $0.01. Bid shading for first-price auctions: shade_factor = p95(historical_clearing_price) / avg(bids_submitted). Typical shade = 85-90% of true value. Win/loss notifications consumed from SSP callback webhooks — win rate and clearing price logged to ClickHouse for bid landscape analysis and future shading calibration.

def run_auction(bids, floor):
valid = [b for b in bids if b.price >= floor]
if not valid: return None
valid.sort(key=lambda b: -b.price)
winner = valid[0]
clear_price = valid[1].price if len(valid)>1 else floor
return AuctionResult(winner=winner, price=clear_price + 0.01)

5 — IVT Post-Win Classifier
LightGBM · 50 Features

Post-win IVT classification runs asynchronously — no impact on bid latency. 50 features: click-to-conversion ratio, geographic velocity (IP location change speed), session length distribution, device fingerprint stability, time-of-day patterns. LightGBM model (300 trees, depth=6) trained on IAB MRC-validated IVT labels. Score >0.8: flag for manual review. Score >0.95: automatic credit-back request to SSP. Model retrained monthly on new labelled data.

def score_impression(impression):
features = extract_features(impression)
ivt_prob = lgb_model.predict([features])[0]
if ivt_prob > 0.95:
credit_back.request(impression.id)
elif ivt_prob > 0.8:
review_queue.add(impression.id)
return ivt_prob

⚖️

Trade-offs & Decision Log

Every senior interview comes down to these decisions. Know the exact trade-off, the reasoning, and the specific numbers that justify each choice.

⚖️ <80ms Total Latency Budget Allocation


Edge Bidder (co-located with SSP) ✅ Chosen
  • Network RTT reduced to <5ms — co-located in same DC as SSP
  • 80ms budget split: 5ms network, 10ms feature lookup, 10ms scoring, 5ms response
  • Allows 60ms for complex ML model inference
  • Requires deploying bidder nodes in every major exchange PoP

Centralised Bidder
  • Single deployment — simpler operations
  • Network RTT alone consumes 40-60ms for cross-region SSPs
  • Only 20ms left for computation — no room for ML models
  • Frequently misses auctions due to timeout

💡

Decision: Edge bidder nodes co-located at SSP PoPs; centralised budget/pacing control plane synced every 1 second

⚖️ Second-Price vs First-Price Auction


Second-Price (Vickrey) — Market standard
  • Dominant strategy: bid true value — no gaming required
  • Well-understood by all DSPs
  • Predictable spend — clear price discovery
  • Header bidding blurs true second price in practice

First-Price ✅ Industry shift
  • Winner pays exactly what they bid — higher spend risk
  • Requires bid shading algorithm to avoid overpaying
  • More transparent — no mystery around clearing price
  • Now dominant in open web (Google, Index Exchange switched 2019)

💡

Decision: Support both; bid shading algorithm (95th percentile of win prices) reduces first-price overpayment to <3%

🎯

Interview Questions — Answered
The exact questions interviewers ask — with production-grade answers

Q1
How is the <80ms latency SLA enforced end-to-end?

Latency budget decomposition: SSP processing (5ms) → network RTT to DSP edge node (5ms) → IVT fraud check (2ms) → feature store lookup (8ms) → budget pacer check (2ms) → bid scoring (8ms) → response serialisation + network back (5ms) = 35ms total DSP processing. 45ms remaining is buffered for SSP overhead and network jitter. Each component has a hard timeout: if the feature store takes >10ms, the bidder uses a cached feature vector. If total time exceeds 75ms, a default bid (or no-bid) is returned immediately. Latency is tracked in a percentile histogram (p50, p95, p99) per SSP partner; any partner with p95>80ms triggers an investigation.

Q2
How does the budget pacer prevent overspending in real time?

Token bucket algorithm per campaign per hour: tokens_available = hourly_budget_remaining × (seconds_remaining_in_hour / 3600). Each winning bid deducts bid_amount from the token bucket. If tokens_available < min_bid (floor), the pacer returns no-bid immediately without consulting the ML scorer. The pacer state is maintained in Redis with millisecond precision. Across distributed bidder nodes, the pacer uses a Redis Lua DECRBY script to atomically check-and-deduct the bid amount — preventing race conditions where two nodes simultaneously win two auctions that together exceed budget. Daily budget pacing uses a smoother: bids are throttled proportionally if spending is 15% ahead of the expected linear pace.

Q3
How does the system detect and filter invalid traffic (IVT)?

Two-stage IVT pipeline: (1) Pre-bid (synchronous, <2ms): IP reputation lookup against a pre-loaded blocklist (IAB Tech Lab IPBL, ~5M IPs); User-Agent bot pattern matching; invalid bid request format check. Bid requests failing pre-bid are no-bid immediately. (2) Post-win (asynchronous, no latency impact): ML classifier (LightGBM) scoring 50+ features: click-to-conversion ratio, geographic consistency, device fingerprint stability, session length. IVT probability >0.8 = flag impression for credit back; >0.95 = automatic credit back without human review. Win events flagged as IVT are credited back within 24h and excluded from attribution.

System Design Series · Every Tuesday & Thursday

Level up your system design interviews

Each post covers Gist, Functional & Non-Functional Requirements, Key Metrics, System Diagram, User Journey, HLD, LLD, and Trade-offs & FAQs.

Subscribe to never miss a post →


Categories: System Design

Tags: , , , , , , ,

Leave a Reply

Discover more from Cloud Wizard Inc.

Subscribe now to keep reading and get access to the full archive.

Continue reading