System Design: Real-Time Bidding (RTB) Platform — Ad Auction in 100ms (2025)

RTB Architecture Overview

Real-Time Bidding is the programmatic ad auction system that runs every time a user loads a web page with an ad slot. Timeline: user visits a page → publisher sends a bid request to an SSP (Supply-Side Platform) → SSP broadcasts the bid request to 10-50 DSPs (Demand-Side Platforms) → each DSP evaluates and responds with a bid in < 100ms → SSP runs the auction (second-price) → winning DSP is notified → ad is served. The entire cycle must complete in < 100-150ms wall-clock time before the page renders, which means DSP bidding logic has a hard budget of ~50ms including network round-trip. This is one of the most latency-sensitive distributed systems at scale: 5-10 million bid requests per second globally.

Bid Request and Response

The OpenRTB standard (IAB) defines the bid request/response schema. Key fields in the bid request: id (auction ID), imp (impression array with ad slot specs: size, floor price, placement type), site/app (publisher URL, category, language), user (user ID, age, gender if available), device (IP, user agent, geo, device type), at (auction type: 1=first-price, 2=second-price). The DSP responds with: id (matches request ID), seatbid[].bid: price (CPM in USD), adid (creative ID), adm (ad markup), nurl (win notification URL), lurl (loss notification URL).

# Bid request processing (DSP side)
class BidEngine:
    def process_bid_request(self, request: BidRequest) -> Optional[BidResponse]:
        start = time.monotonic_ns()

        # 1. User lookup: fetch user profile from Redis ( 45:  # timeout guard
            return None

        return BidResponse(
            id=request.id,
            seatbid=[SeatBid(bid=[Bid(
                price=bid_price,
                adid=best_campaign.creative_id,
                nurl=f"https://dsp.example.com/win?aid={request.id}&price=${{AUCTION_PRICE}}"
            )])]
        )

Budget Pacing with Redis

Advertisers set daily budgets. DSPs must pace spend evenly (not blow the budget in the first hour). Token bucket pacing: each campaign has a Redis counter for tokens. A background job refills tokens every second: refill_rate = daily_budget / (86400 * avg_cpm / 1000). On each bid: DECRBY the tokens counter by the bid price. If tokens 1.0, slow down; if < 0.8, speed up.

Win/Loss Notification and Attribution

When the SSP selects a winner: the SSP fires the win notification URL (nurl) with the clearing price substituted for ${AUCTION_PRICE}. The DSP receives this HTTP callback and: (1) records the win and clearing price, (2) deducts the actual clearing price from the budget (vs. the bid price deducted optimistically), (3) credits back the difference. For second-price auctions, the clearing price < bid price in most cases. Loss notifications (lurl) are optionally fired for losing bids. Attribution: when the served ad is clicked or viewed, the DSP fires impression/click pixels, logging event data to Kafka. Attribution joins click events to the original bid record to credit conversions to the correct campaign.

Frequency Capping

Frequency cap: limit how many times a user sees an ad (e.g., max 3 impressions per day per campaign). Implementation: Redis sorted set per (user_id, campaign_id) with impression timestamps. On each bid: ZRANGEBYSCORE to count impressions in the last 24 hours. If count >= cap: do not bid. ZADD the timestamp on win notification. Expiry: ZREMRANGEBYSCORE to remove entries older than 24 hours. TTL on the key = 25 hours. Cross-device frequency capping: use a device graph to link user IDs across devices and aggregate frequency counts. For very high scale (100M+ users): use a Bloom filter or Count-Min Sketch per campaign to approximate frequency — allows false positives (skip some valid impressions) but prevents over-serving with O(1) operations.

See also: Snap Interview Prep

See also: Twitter/X Interview Prep

See also: Meta Interview Prep

Scroll to Top