AdTech
Header Bidding with Prebid: How It Works and Why It Breaks
The Prebid.js auction flow, a minimal working setup, the ad-server hand-off and a debugging guide for the failures publishers hit most.
Header bidding lets a publisher offer the same impression to many demand sources at once, then choose the best result, instead of asking them one at a time in a fixed order. Prebid is the open-source standard for doing this. Its power is also the source of its difficulty: it runs in the visitor's browser, depends on several third parties, interacts with the ad server, and has to finish before the page feels slow. This guide explains the flow, a minimal setup and, most usefully, how to diagnose problems.
The auction flow
- Prebid loadsThe Prebid library, with the bidder adapters you chose at build time, loads on the page, typically asynchronously.
- Ad units are declaredEach slot is described by an ad unit: a code, the media types and sizes, and the list of bidders with their parameters.
- Bid requests go out in parallelEach adapter sends a request to its demand partner, either directly from the browser or through a server-side Prebid Server.
- Responses are collectedBids that arrive before the bidder timeout are accepted. Late or invalid bids are dropped.
- A winner is chosen per slotPrebid compares the valid bids, after applying floors and any adjustments, and picks the highest.
- Targeting is set on the ad server requestThe winning bid's price bucket and identifiers are passed as key-values.
- The ad server decidesThe ad server (commonly Google Ad Manager) compares the header-bidding price with its own direct and programmatic demand.
- RenderIf the header-bidding bid wins, a Prebid creative in the ad server calls back to Prebid to render the winning ad.
A minimal working setup
var pbjs = pbjs || {};
pbjs.que = pbjs.que || [];
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];
var PREBID_TIMEOUT = 1500; // bidders must respond in this time
var FAILSAFE_TIMEOUT = 3000; // ad server is called regardless after this
var adUnits = [{
code: "div-gpt-ad-top", // must match the GPT slot div id
mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } },
bids: [
{ bidder: "appnexus", params: { placementId: 13144370 } }, // test placement from the docs
{ bidder: "exampleBidder", params: { siteId: "abc123" } } // illustrative only
]
}];
pbjs.que.push(function () {
pbjs.setConfig({
bidderTimeout: PREBID_TIMEOUT,
priceGranularity: "medium", // $0.10 buckets up to $20
enableSendAllBids: false
});
pbjs.addAdUnits(adUnits);
pbjs.requestBids({ bidsBackHandler: sendAdserverRequest });
});
var adserverRequestSent = false;
function sendAdserverRequest() {
if (adserverRequestSent) return; // guard against double calls
adserverRequestSent = true;
pbjs.que.push(function () {
pbjs.setTargetingForGPTAsync(); // sets hb_pb, hb_bidder, hb_adid, ...
googletag.cmd.push(function () { googletag.pubads().refresh(); });
});
}
setTimeout(sendAdserverRequest, FAILSAFE_TIMEOUT);
Four ideas in this snippet deserve attention:
- The ad unit
codelinks Prebid's slot to the page's ad slot. If it does not match the slot's element id, bids never reach the ad server for that slot. bidderTimeoutis the maximum time bidders have. A longer timeout allows slower bidders to participate, which can raise revenue, but delays the ad call and can harm user experience and viewability.priceGranularitycontrols how a bid price is rounded into a bucket. The built-in "medium" setting uses ten-cent steps up to twenty dollars. The ad server needs matching line items for those buckets.- The failsafe timeout guarantees that the ad server is called even if Prebid hangs, so a problem with a single bidder cannot blank the page's ads.
The ad-server hand-off
When the auction ends, Prebid sets key-values on the ad slot, the key ones being:
| Key | Meaning |
|---|---|
hb_pb | Price bucket of the winning bid, for example 1.50 |
hb_bidder | Which bidder won |
hb_adid | Identifier used by the creative to ask Prebid to render the right ad |
hb_size | Size of the winning creative |
hb_format / hb_source | Media type and whether the bid came from the client or a server |
In the ad server, you create line items that target hb_pb values, each priced at that bucket. If a bid's bucket has no matching line item, the ad server cannot select it and the bid is effectively lost, with no error. This is why changing price granularity requires updating the ad-server setup in step.
Client-side versus server-side
In client-side bidding, the browser sends a request to each bidder. It is transparent and easy to debug, but each extra bidder adds network connections and page weight. In server-side bidding, using Prebid Server, the browser makes one request and the server fans out to many bidders. That reduces browser load and tends to scale better with many partners, at the cost of cookie-matching complexity and slightly less visibility. Many publishers run a hybrid: a handful of high-value bidders client-side, and the long tail server-side.
Consent and identity
Prebid has modules that read the user's consent signal and pass it to bidders, and that gate which bidders and identifiers may be used. If a consent module is configured but your consent management platform has not yet produced a string when the auction starts, bidders may be blocked or may withhold bids. Test the auction both with consent granted and denied, and in each region where you have traffic. Identity modules add user identifiers to requests where permitted, which can increase bid rates and prices; they also add dependencies that need monitoring.
The failures that cost the most
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Bidder never appears | Adapter not included in the build, or invalid parameters | Look for validation warnings in debug mode; check the build's module list |
Many bidTimeout events | Timeout too short for the bidder, slow network, or too many bidders | Compare timeToRespond with the timeout; test on a slower connection |
| Bids exist but nothing renders | No ad-server line item for the price bucket, or slot code mismatch | Compare getAdserverTargeting() with line-item targeting |
| Ad server called before bids returned | Race between ad server and Prebid, or missing failsafe | Check the order of calls in the network panel |
| Zero bids from a region | Consent not available, or bidder does not serve that region | Run with a VPN and consent states; inspect consent module logs |
| Wrong creative size | Bid size not in the slot's size list, or the ad server slot has different sizes | Align ad unit sizes with the ad server slot and the line item |
| Revenue drop after a release | Config change: timeouts, floors, bidder removal, or a Prebid version upgrade | Diff configuration; compare bid rate, timeout rate and CPM by bidder |
| Slots refresh with no bids | Refresh logic does not rerun the auction | Ensure each refresh calls requestBids for the slot first |
Debugging with the browser console
Prebid is generous with debugging tools. Append ?pbjs_debug=true to a page URL to enable verbose logging, then use the global API and events:
// Add ?pbjs_debug=true to the page URL, then in the browser console:
pbjs.getBidResponses(); // every bid received, per ad unit
pbjs.getAllWinningBids(); // bids that rendered
pbjs.getAdserverTargeting(); // the key-values sent to the ad server
// Listen to events to see timing and failures
pbjs.onEvent("bidRequested", e => console.log("requested", e.bidderCode));
pbjs.onEvent("bidResponse", b => console.log("response", b.bidder, b.cpm, b.timeToRespond + "ms"));
pbjs.onEvent("noBid", b => console.log("no bid", b.bidder));
pbjs.onEvent("bidTimeout", t => console.log("timeout", t.map(x => x.bidder)));
pbjs.onEvent("auctionEnd", a => console.log("auction end", a.auctionId));
A good first investigation compares three views of the same auction. What did each bidder return (getBidResponses)? What did Prebid send to the ad server (getAdserverTargeting)? And what did the ad server choose? The difference between any two adjacent views shows where the value was lost.
Monitoring in production
Because header bidding is distributed across browsers, you rarely notice problems by looking at a single page. Collect auction data with an analytics adapter, and track at least these metrics by bidder, device type and region: bid rate, timeout rate, average CPM, response latency, and win rate. Alert on sudden changes, since a bidder silently failing or a new release that increases timeouts is far more common than a catastrophic outage. Finally, keep your ads.txt up to date for every bidder, since an unauthorised seller will reduce bids regardless of how well the technical setup works. See our guide to ads.txt and sellers.json.
Key takeaways
- Prebid runs a parallel auction in the browser (or on a server), then hands the winning bid to the ad server as targeting values.
- The ad server needs line items that match the price buckets and key-values; without them a winning bid never renders.
- Always set a failsafe timeout so a slow auction cannot prevent the ad server call.
- Timeout, consent, size and ad unit code mismatches cause most lost revenue. Check them first.
- Use debug mode, the global API and Prebid events to see exactly what each bidder did.
Related guides
Further reading
About this guide. Written by the Kensaltensi engineering and QA team for educational purposes. Examples are illustrative, and standards evolve, so check the primary sources above before relying on a detail in production. If you spot an error or an out-of-date statement, email hello@kensaltensi.org and we will review and correct it.