How an OpenRTB auction runs from page load to ad render

An OpenRTB auction is one fast HTTP round trip with slower events on either side of it. The seller builds a bid request, bidders answer inside tmax, the exchange picks a winner, and the win, billing, and loss notices then arrive on their own schedules, sometimes minutes later. This page follows one banner impression through every step and shows where the time goes.

The loop at a glance

Only the bidding window is governed by tmax. Everything after the auction runs on exchange policy and on what the device does with the ad.

StepWho actsWhat movesBound by
1. Ad callPage, app, or playerTag, SDK call, or VAST request to the sellerThe publisher's own timeout
2. Request buildSSP or exchangeA BidRequest built from the ad call plus lookupsThe seller's internal budget
3. Fan-outExchangeHTTP POST to each eligible bidderThe tmax clock starts
4. BiddingBiddersA BidResponse with HTTP 200, or an empty 204tmax
5. CloseExchangeLate responses droppedtmax
6. AuctionExchangeChecks, filters, floors, ranking, clearing priceA few milliseconds of exchange compute
7. WinExchange to winnernurl call, markup handed to the sellerRight after the auction
8. RenderDeviceCreative loads and displaysThe page, app, or player
9. BillingExchange to winnerburl call when the impression countsExchange billing policy
10. LossExchange to loserslurl call with a reason codeExchange loss notice policy

OpenRTB itself covers steps 3 through 10. The ad call in step 1 belongs to whatever the seller exposes to publishers: a page tag, Prebid.js, a mobile SDK, or a player's ad request. Who plays which role is covered in SSP, DSP, and ad exchange.

Steps 1 and 2: the ad call becomes a bid request

When the page loads, an ad tag asks the seller for an ad. The seller's server then adds what no bidder could know on its own. It resolves the IP address to a location, looks up each bidder's user ID from its cookie sync table, reads the consent signal the page passed along, applies the publisher's floor rules, and appends its own node to the supply chain. The result is one JSON object per auction.

{
  "id": "b7e2c1d4-58f3-4a9e-9c1b-2f6d8e0a7c35",
  "at": 1,
  "tmax": 200,
  "cur": ["USD"],
  "imp": [
    {
      "id": "1",
      "tagid": "home-leaderboard",
      "secure": 1,
      "bidfloor": 1.10,
      "bidfloorcur": "USD",
      "banner": {
        "format": [{ "w": 728, "h": 90 }, { "w": 970, "h": 90 }],
        "pos": 1
      }
    }
  ],
  "site": {
    "id": "site-2214",
    "domain": "recipes.example",
    "page": "https://recipes.example/weeknight/lemon-pasta",
    "publisher": { "id": "pub-7731" }
  },
  "device": {
    "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
    "ip": "198.51.100.23",
    "devicetype": 2
  },
  "user": { "buyeruid": "u-5f81c0e2" },
  "source": {
    "fd": 1,
    "tid": "0f3e9a7c-2b41-4d8e-a6c5-91d7e2b48f10",
    "schain": {
      "ver": "1.0",
      "complete": 1,
      "nodes": [{ "asi": "ssp.example", "sid": "pub-7731", "hp": 1 }]
    }
  },
  "regs": { "coppa": 0 }
}

Three fields set the rules before any bidder reads the request. at of 1 means the winner pays its own bid. tmax of 200 gives bidders 200 ms, network included. imp.bidfloor of 1.10 with imp.bidfloorcur USD is the minimum CPM. The request id may differ for each bidder the exchange calls, while source.tid stays the same for every party that sees this impression, which is what lets buyers spot the same impression arriving through two sellers.

The example sets source.fd to 1. That tells bidders a decision upstream, here a header bidding wrapper and the publisher's ad server, can still override this exchange's result. In that setup the whole OpenRTB auction runs inside the wrapper's timeout, which is why sellers in header bidding send tight tmax values. The layering is explained in OpenRTB vs Prebid.

Step 3: fan-out over HTTP

The exchange sends the request to every eligible bidder in parallel. OpenRTB requires HTTP POST for bid requests, and JSON is the standard body. A typical set of headers:

POST /openrtb2/bid HTTP/1.1
Host: bidder.example
Content-Type: application/json
Accept-Encoding: gzip
Connection: keep-alive
x-openrtb-version: 2.6

The x-openrtb-version header lets the bidder choose a parser before it reads the body. It names the version line, so 2.6 traffic from any dated release says 2.6. Accept-Encoding: gzip invites a compressed response; compressing the request body as well needs prior agreement between the two parties. Persistent connections matter most: a fresh TCP and TLS handshake on every auction can cost more than the bidder's entire decision.

Fan-out is not always a copy of one request. The exchange may strip identifiers a bidder is not allowed to receive under the consent signal, or drop fields a partner cannot parse. Each bidder sees its own version of the auction, never the seller's raw input.

Steps 4 and 5: where tmax goes

The tmax clock runs at the exchange, from the moment the request leaves to the moment the response arrives. Network time in both directions counts against it. An illustrative budget for the request above:

tmax = 200 ms, measured by the exchange
  request on the wire, exchange to bidder        25 ms
  bidder: accept, decompress, parse               5 ms
  bidder: targeting, pricing, creative choice    90 ms
  bidder: serialize and compress                  3 ms
  response on the wire, bidder to exchange       25 ms
  left for jitter and the slow tail              52 ms

The bidder controls only the middle lines. A bidder 60 ms of round trip away from the exchange has spent 30 percent of this budget before it reads a byte, which is why bidders deploy close to the exchanges they buy from. A bidder that knows it cannot finish in time does better returning a no-bid at once, or a BidResponse with BidResponse.nbr 15 (Insufficient Auction Time), than sending a bid that will be discarded.

Every extra hop eats the same budget. An intermediary that resells the impression to another exchange should forward a smaller tmax than it received, after subtracting its own processing and the added network leg. OpenRTB 3.0 states that rule outright, and it applies equally to 2.x chains. The full definition and typical values are in the tmax reference.

What happens on timeout

At the deadline the exchange closes the auction for that impression. A response that arrives afterward is dropped unread. No error comes back, no loss notice fires, and no reason code is sent, because the bid never entered the auction. The exchange counts a timeout in its own reporting, and the bidder only finds out by comparing its response times with tmax or by asking the exchange for its timeout rate.

A timeout is a separate outcome from the two deliberate answers. A bidder that does not want the impression returns HTTP 204, or a BidResponse with an nbr code. A bidder that receives a malformed request should answer HTTP 400 with no body. All three look like a missing bid in a win-rate chart, but they need different fixes, and many exchanges send less traffic to a bidder whose timeout rate stays high. The debugging sequence is in tmax and bidder timeouts.

Step 6: the auction

Between the close and the win, the exchange works through the bids it received. The order varies by exchange, but the stages are the same, and each one maps to a block of loss reason codes.

  1. Response checks. BidResponse.id must echo the request ID, each bid.impid must match an imp.id, the currency must be one listed in cur, and bid.mtype must name a media type the impression offered. Failures land in codes 1 to 10.
  2. Policy filters. bcat, badv, and bapp on the request, imp.banner.battr on the impression, plus the exchange's creative review. Failures land in the 200 range.
  3. Floors. The bid, converted to the floor's currency, must clear imp.bidfloor or the floor of the deal it names. Failures are 100 and 101.
  4. Ranking. Deal priority, then price. A seat that set seatbid.group to 1 on a multi-impression request wins all of its impressions or none. Losers here get 102 or 103.
  5. Clearing price. With at 1 the winner pays its bid. With at 2 it pays a small increment over the runner-up. From 2.6-202606 the spec also states that ${AUCTION_PRICE} is the price after any seller discount.

Auction types are compared in first-price vs second-price, and every code is listed in loss reason codes.

Step 7: the win notice and the markup

A winning bid for the request above might look like this. The three notice URLs carry macros that the exchange fills in at the moment it calls each one:

{
  "id": "b7e2c1d4-58f3-4a9e-9c1b-2f6d8e0a7c35",
  "cur": "USD",
  "seatbid": [{
    "seat": "agency-311",
    "bid": [{
      "id": "bid-1",
      "impid": "1",
      "price": 2.40,
      "adm": "<a href='https://advertiser.example/lp'><img src='https://cdn.dsp.example/c/88.png' width='728' height='90'></a>",
      "adomain": ["advertiser.example"],
      "crid": "cr-88",
      "mtype": 1,
      "w": 728,
      "h": 90,
      "nurl": "https://dsp.example/win?a=${AUCTION_ID}&i=${AUCTION_IMP_ID}&p=${AUCTION_PRICE}",
      "burl": "https://dsp.example/bill?a=${AUCTION_ID}&p=${AUCTION_PRICE}",
      "lurl": "https://dsp.example/loss?a=${AUCTION_ID}&r=${AUCTION_LOSS}&m=${AUCTION_MIN_TO_WIN}"
    }]
  }]
}

nurl fires once the exchange has a winner, usually from the exchange's servers within milliseconds. Because this bid carries its creative in adm, the exchange can hand the markup to the seller and call the win notice at the same time. A bidder that serves markup on the win notice instead puts that call on the critical path: the ad cannot render until the bidder answers, and an HTTP failure forfeits an impression it already won. The tradeoff is covered in adm vs nurl.

A win notice means this bid won this exchange's auction. It does not mean the ad rendered, was seen, or will be billed. With source.fd at 1, the publisher's ad server can still choose something else. Some exchanges fire nurl on their own auction result and others wait for the upstream decision, and the spec asks each exchange to publish what its win notice means. Read that document before building reports on nurl.

Steps 8 and 9: render and the billing notice

The markup travels back through the seller to the device and renders. For a web banner that is usually well under a second after the auction. It can take much longer. A game may prefetch an interstitial during play and show it when the level ends, several minutes after the auction. A DOOH screen may display the ad at a scheduled time, which the seller can announce with imp.dt.

burl fires when the win becomes billable under the exchange's policy, typically on render or a counted impression. For VAST video the VAST impression event is the billing signal, and burl should fire with it. The spec recommends firing it server-side, close to where the exchange books revenue, which keeps exchange and bidder counts aligned. Bidders should count spend on burl and treat nurl as a pricing signal.

Three fields describe how long the gap may be. imp.exp is the exchange's advisory number of seconds between auction and impression. bid.exp is how long the bidder is willing to wait. The ${AUCTION_IMP_TS} macro lets a platform that cannot fire right away report when the impression actually happened. The implementation guide advises bidders to track the delay between auction and billing notice, since late billing distorts pacing and frequency caps. OpenRTB 3.0 adds two practices that suit 2.x as well: retry a billing call that fails, and make the billing endpoint idempotent so a retry is never counted twice.

Step 10: loss notices

When the exchange knows a bid lost, it may call that bid's lurl with ${AUCTION_LOSS} replaced by a loss reason code. Exchanges that support ${AUCTION_MIN_TO_WIN} also say what price would have tied the winner, which is the most useful number a first-price bidder can get. Policy may blank the price macros, and some exchanges batch or sample loss notices rather than firing them in real time.

Loss notices are optional, so a missing notice is not a win. They also cover only the exchange's own decisions: a bid that won the exchange auction and then lost in the publisher's ad server gets no OpenRTB loss notice. The full code list and how to read it are in loss reason codes, and the macro rules are in substitution macros.

Check one round trip

The request and bid on this page form a valid pair. To check a pair of your own, load the matched pair sample in the tester and replace it with your payloads.

Questions people ask

Does tmax include the time a bid request spends on the network?

Yes. BidRequest.tmax is the number of milliseconds the exchange waits for bids, measured at the exchange and including internet latency in both directions. With tmax at 200 and 25 ms of network each way, a bidder has about 150 ms for parsing, pricing, and serializing, and less once it keeps a margin for slow responses.

What does a bidder get back when its response misses tmax?

Nothing. The exchange closes the auction at the deadline and drops late responses unread. No loss notice fires and no reason code is sent, because the bid never entered the auction. The exchange records a timeout in its own reporting, so the bidder has to compare its response times with tmax or ask the exchange for timeout rates.

When does the billing notice fire compared to the win notice?

The win notice (nurl) fires as soon as the exchange picks the winner. The billing notice (burl) fires later, when the impression becomes billable under the exchange's policy, usually when the ad renders or, for VAST video, at the VAST impression event. For prefetched app inventory that gap can be minutes, and imp.exp tells bidders how many seconds it may be.