0) Problem Restatement
Advertisers create campaigns with a budget (e.g., $70,000), a flight (start and end dates, e.g., 7 days), targeting, and a delivery goal (impressions or spend). Without pacing, a campaign that wins many auctions could spend everything in the first few hours (a waste: bad timing and less reach). Pacing spreads delivery smoothly over the flight, following when viewers actually watch, and adapts to traffic changes. Netflix asked this for its video ads platform.
1) Requirements
- Deliver close to 100% of the budget by the end of the flight, without going over (or within a tiny tolerance).
- Spread delivery following the audience's traffic curve (more in the evening, less at 4 AM).
- Adapt quickly to changes: traffic spikes, targeting changes, budget edits.
- Work across hundreds of ad servers deciding in parallel, each in milliseconds.
2) The Core Idea
- Plan: compute an ideal cumulative spend curve for the flight, using a traffic forecast. For example, if 8–10 PM usually has 20% of the day's matching traffic, plan 20% of the daily budget there.
- Measure: track actual spend in near real time.
- Control: every minute, compare actual vs planned. If spending is ahead, slow down. If behind, speed up.
How do we slow down or speed up? Two common "knobs":
- Throttling probability (pacing rate): the campaign only enters a fraction
pof eligible auctions.pis adjusted every minute. - Bid shading: lower or raise the bid, which wins fewer or more auctions (used in auction-based platforms).
3) Architecture
Architecture Diagram
flowchart LR
AS["Ad servers - use pacing rate p"] -->|"impressions, spend"| K[("Kafka - delivery events")]
K --> SPD["Spend aggregator - per campaign per minute"]
SPD --> ST[("Spend store")]
FC["Traffic forecaster"] --> PLAN["Plan: target curve per campaign"]
PLAN --> CTRL["Pacing controller - every minute"]
ST --> CTRL
CTRL -->|"new p per campaign"| CFG[("Pacing config")]
CFG -->|"push"| AS4) The Controller (simple and explainable)
Every minute, for each campaign:
target_so_far = planned cumulative spend at this time
actual_so_far = measured spend
error = (target_so_far - actual_so_far) / remaining_budget
p_new = clamp(p_old × (1 + k × error), p_min, 1.0)
- This is a proportional controller (a PID controller without the I and D parts). Adding some smoothing avoids oscillation.
- Also recompute the plan for the remaining time and budget (e.g., if we underspent yesterday, spread the leftover over the remaining days rather than dumping it in one hour).
5) Avoiding Overspend Across Many Servers
- Spend data arrives with a delay (seconds to a minute), and hundreds of ad servers serve at once. Near the end of the budget, they could all keep serving and overspend.
- Budget slices: the controller gives each ad server (or region) a small allowance for the next minute. Servers stop the campaign locally when their slice is used up, and refill each minute.
- Safety margin: as the remaining budget gets small, reduce the slices and
pmore aggressively, and stop at ~99.x% if needed. - Hard stop: a separate fast check blocks a campaign when total spend reaches the budget, and it's pushed to all servers immediately.
6) Forecasting and Goals
- Traffic forecast: from historical impressions for the campaign's targeting (by hour of day and day of week), updated with recent trends.
- Goals: pacing by spend (most common), by impressions, or with frequency and reach goals (combined with frequency capping).
- End of flight: allow slightly faster delivery near the end if behind (within limits), so the campaign doesn't under-deliver.
7) Trade-offs & Alternatives
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Knob | Throttle probability | Simple, works for fixed-price ads | Bid shading: better for auctions, more complex |
| Control loop | Per-minute proportional controller + re-plan | Adapts, explainable | Fixed hourly caps: can't adapt |
| Overspend protection | Budget slices + hard stop | Tight control with delayed data | Rely on delayed spend only: overspend |
| Plan | Traffic-weighted curve | Matches when viewers watch | Even split per hour: wasteful at night |
8) Wrap-Up
Plan a target cumulative spend curve from a traffic forecast, measure spend in near real time from delivery events, and run a per-minute controller that adjusts each campaign's throttle probability (or bid) based on how far ahead or behind plan it is, re-planning the remaining budget. Prevent overspend with per-server budget slices, tighter control near the end, and a fast hard stop pushed to all ad servers.