An advertiser sets a $1,000 daily budget. Without pacing, the DSP would spend the entire budget in the first hour of the day, when bid prices happen to be low, and the ad would show to nobody for the remaining 23 hours. Budget pacing distributes ad spend evenly across the day (or according to a target schedule) so the advertiser gets coverage throughout the day rather than a burst.

The Pacing Problem#

Pacing is a rate control problem with a stochastic input. You don’t know how many bid requests will arrive. You don’t know which ones you’ll win. You need to spend $1,000 across 24 hours, which means roughly $41.67 per hour, but bid opportunities arrive unevenly. Traffic is higher at 8 PM than at 3 AM.

Simple throttling: bid on X% of opportunities, where X is adjusted periodically. If you’re spending too fast, lower X. If underspending, raise X.

Smooth pacing algorithm: set a target spend rate (dollars per second). Track actual spend. When actual spend is ahead of target, increase the probability of skipping a bid. When behind, decrease it. This is a feedback loop: desired_skip_rate = (actual_spend - target_spend) / target_spend.

graph TD A[Bid request arrives] --> B[Check pacing controller] B --> C{Actual spend vs target?} C --> |Ahead of schedule| D[Increase skip probability: bid on 60% of requests] C --> |Behind schedule| E[Decrease skip probability: bid on 95% of requests] C --> |On track| F[Bid on 80% of requests] D --> G[Apply skip probability: random sample] E --> G F --> G G --> H{Skip this bid?} H --> |Yes| I[Return no-bid to exchange] H --> |No| J[Proceed to targeting and bid calculation] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style H fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style I fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style J fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Distributed Budget Tracking#

The DSP runs on hundreds of servers. Each server bids independently. Budget spend must be tracked across all servers. A centralized counter in Redis: INCRBY campaign:budget_spent:123 150 (spent 150 cents on this bid). Each server checks the counter before bidding and skips if the budget is exhausted.

At 500,000 bid requests per second across 200 servers, that’s 2,500 Redis writes per second per campaign. For large advertisers with hundreds of campaigns, Redis becomes the bottleneck. Solution: local counters on each server, synchronized to Redis every 100ms. Each server operates with a local budget slice (1/N of the total) and rebalances periodically.

Overspend and Underspend#

Pacing is never perfect. Two failure modes: overspend (served too many ads, charged over budget) and underspend (budget not fully utilized, advertiser gets less reach than expected). Overspend is worse for trust. Most systems tolerate small overspend (10%) with a compensating credit to the advertiser. Underspend is bad for advertiser ROI.

At Oracle#

Oracle Marketing Cloud had campaign budget controls for email sends: send no more than N emails per day from a campaign. We used a Redis counter with daily TTL. Simple, but the counter wasn’t distributed — all send workers shared one Redis key. At high send volumes, the counter became a hot key. We partitioned it: 10 counters per campaign, each worker picking one randomly. Aggregate across all 10 for reporting. Hot key problem gone, reporting added 10 reads instead of 1.

What I’m Learning#

Budget pacing is rate control with a target total rather than a target rate. The distributed counter pattern is the core primitive, and the hot key problem always appears when a counter is shared across many writers. Partitioning the counter trades read complexity for write throughput.

Have you implemented distributed counters at scale, and how did you handle the consistency vs throughput trade-off?