What this page covers
- Prebid.js addAdUnits and setConfig with annotated examples.
- Auction flow from page load through GAM key-value targeting.
- Price granularity, user sync, timeout strategy, and common failures.
Learning Track
Master the header bidding auction: adapter setup, price granularity, timeout strategy, and debugging bid failures.
Intermediate track
This page assumes you know the general header bidding concept and want to go deeper: how Prebid auctions actually execute, how to configure adapters and price granularity, and how to debug timeout or no-bid issues in real deployments.
Prebid is the open-source header bidding wrapper used by the majority of large publishers. It runs an auction among multiple SSPs/DSPs before the ad server (GAM) is called, ensuring the publisher gets the highest possible CPM. When it is misconfigured — wrong timeout, bad price granularity, stale adapter — publishers leave significant revenue on the table without any obvious error message. TAMs are often the first escalation point for bid discrepancies and timeout complaints from partners.
pbjs.que.push() defers execution until Prebid.js is ready.bidderTimeout clock starts.bidderTimeout ms are stored. Late bids are dropped with no error shown to end user.hb_pb=2.80, hb_bidder=appnexus) on the GPT ad request via setTargetingForGPTAsync().pbjs.renderAd() which serves the DSP’s ad markup into the slot.Defines which ad units run header bidding and which bidders compete. Each bidder has adapter-specific params.
pbjs.addAdUnits([
{
code : 'div-gpt-ad-300x250' , // Must match the GPT ad slot div ID on the page
mediaTypes : {
banner : {
sizes : [[300 , 250 ], [300 , 600 ]] // All sizes the slot accepts — more sizes = more demand
}
},
bids : [
{
bidder : 'appnexus' ,
params : { placementId : 12345678 } // Find in AppNexus console under Placements
},
{
bidder : 'rubicon' ,
params : {
accountId : 9001 ,
siteId : 283 ,
zoneId : 1234 // All three are required for Rubicon
}
},
{
bidder : 'ix' , // Index Exchange — requires explicit size param
params : {
siteId : '47777' ,
size : [300 , 250 ] // IX does not infer size from mediaTypes
}
},
{
bidder : 'pubmatic' ,
params : {
publisherId : '158355' ,
adSlot : 'homepage-mrec@300x250' // PubMatic slot name includes size annotation
}
}
]
},
{
code : 'div-gpt-ad-video' ,
mediaTypes : {
video : {
context : 'instream' , // 'instream' = inside video player; 'outstream' = injected player
playerSize : [[640 , 480 ]],
mimes : ['video/mp4' ],
protocols : [1 , 2 , 3 , 4 , 5 , 6 ],
playbackmethod : [2 ], // 2 = auto-play with sound on
skip : 1 ,
skipafter : 5 // User can skip after 5 seconds
}
},
bids : [
{
bidder : 'appnexus' ,
params : {
placementId : 98765432 ,
video : {
skippable : true ,
playback_method : ['auto_play_sound_on' ]
}
}
}
]
}
]);
instream means the ad plays inside an existing video player. outstream means Prebid injects its own player into the page content. Setting the wrong context causes adapters to reject or ignore the ad unit.Controls timeout, price buckets, currency, user sync, and identity. Call once before requestBids.
pbjs.setConfig({
bidderTimeout : 1500 , // ms — adapters not responding by this time are dropped
// Under 800ms = high timeout rate; over 2000ms = slow page
enableSendAllBids : false , // false = only winning bid key-values sent to GAM (cleaner)
// true = all bidder key-values sent (hb_pb_appnexus, hb_pb_rubicon...)
priceGranularity : {
buckets : [
{ max : 5 , increment : 0.05 }, // $0–$5 in 5-cent steps (100 buckets)
{ max : 20 , increment : 0.10 }, // $5–$20 in 10-cent steps (150 buckets)
{ max : 100 , increment : 0.50 } // $20–$100 in 50-cent steps (160 buckets)
]
// Custom granularity prevents revenue leakage from bucket rounding.
// A $2.49 bid with 'medium' granularity rounds to $2.40 — 9 cent/CPM loss at scale.
},
userSync : {
syncEnabled : true ,
filterSettings : {
iframe : {
bidders : ['appnexus' , 'rubicon' , 'pubmatic' ],
filter : 'include' // Whitelist these adapters for iframe syncs
},
image : {
bidders : '*' , // Allow all adapters to fire image pixel syncs
filter : 'include'
}
},
syncsPerBidder : 5 , // Max sync calls per adapter per page load
syncDelay : 3000 // ms after auction before syncs fire — prevents latency impact
},
currency : {
adServerCurrency : 'USD' , // Normalize all bid prices to this for GAM key-value targeting
granularityMultiplier : 1
},
schain : { // Supply chain — required by many DSPs
validation : 'strict' ,
config : {
ver : '1.0' ,
complete : 1 ,
nodes : [{ asi : 'your-exchange.com' , sid : 'pub-456' , hp : 1 }]
}
},
debug : false // Set to true in dev — verbose auction log in console
});
pbjs.que.push(function () {
pbjs.setConfig({ bidderTimeout: 1500 });
pbjs.addAdUnits(adUnits);
pbjs.requestBids({
timeout : 1500 ,
bidsBackHandler : function (bids) {
// Called when all bids are in OR timeout fires — whichever is first
pbjs.setTargetingForGPTAsync(); // Writes hb_pb, hb_bidder, hb_adid to GPT slot targeting
googletag.pubads().refresh(); // Fires GAM ad request with Prebid key-values attached
}
});
});
// === Browser console debugging commands ===
// pbjs.getBidResponses() → all bids per ad unit (including status)
// pbjs.getHighestCpmBids() → winning bid per unit
// pbjs.getBidResponsesForAdUnitCode('X') → bids for a specific div ID
// pbjs.getAdserverTargeting() → key-values set on GPT slots
| Dimension | Prebid.js (client-side) | Prebid Server (server-side) |
|---|---|---|
| Where auction runs | User’s browser / device | Cloud server (self-hosted or via AppNexus, Rubicon) |
| Page latency | Higher — JS executes in browser, each adapter fires separately | Lower — one server-to-server call replaces N adapter calls |
| Cookie sync / ID matching | Full browser cookie access — highest match rates | Limited — server has no browser cookies; relies on eids |
| CTV / OTT support | Not applicable — no JS runtime on Smart TVs or STBs | Primary method — CTV apps call PBS API directly |
| Adapter count | 350+ adapters in the Prebid.js GitHub repo | Separate adapter repo — growing but fewer total |
| Configuration format | JavaScript on the page | Server YAML config + stored requests in database |
| Best debugging tool | Browser console + Prebid Analyst Chrome extension | Server logs + direct /openrtb2/auction endpoint testing |
One adapter consistently times out while others return bids. The adapter is making slow calls, the DSP endpoint is latency-heavy, or the endpoint URL is wrong in the adapter params.
▸ Run pbjs.getBidResponses() — look for “Timed out” on that bidder. Test raising bidderTimeout to 2000 ms. If still timing out, capture a HAR trace and escalate to the adapter team with p95 latency data.
Prebid wins the auction but GAM serves a lower-priority line item. Usually a key-value targeting mismatch: the line item targets hb_pb=2.80 but Prebid sets hb_pb=2.75 due to bucket rounding.
▸ Open GPT console (googletag.openConsole()) → Targeting tab → confirm hb_pb value. Compare against GAM line item key-value ranges. Adjust price granularity buckets to cover the actual bid values.
Adapter is responding but the DSP is not competing. Possible causes: currency conversion failure, floor price mismatch, or DSP deliberately under-bidding to win at near-zero cost.
▸ Check pbjs.getBidResponses() for the adapter. Confirm currency config matches DSP. Compare adapter bid against bidfloor. Contact DSP — confirm their targeting and minimum bid for your inventory.
requestBids fires but bidsBackHandler receives an empty bids object. All adapters either timed out or returned no-bids. Usually a config error in addAdUnits or the Prebid.js file failed to load entirely.
▸ Check browser console for JS errors. Confirm pbjs.adUnits is populated. Enable debug: true in setConfig and review the verbose auction log for per-adapter rejection reasons.
Adapter bids are lower than expected because users are unknown to the DSP. The cookie sync iframe never fired — caused by overly restrictive filterSettings or CMP blocking iframe calls before consent is given.
▸ Check Network tab for /usersync endpoints. Confirm CMP consent includes Purpose 1 (storage). Verify that filterSettings includes the adapter in the iframe or image sync allowlist.
Same creative renders twice, or a bid from a previous page load wins the current auction. Caused by enableSendAllBids: true writing multiple conflicting key-values, or bid caching enabled without cache TTL aligned to auction cycle.
▸ Switch to enableSendAllBids: false. If using Prebid cache, set cache TTL to match the page’s expected ad refresh interval. Confirm GAM line item creative rotation is set to “Optimize” not “Even”.
| Preset | Range | Increment | Buckets | Use case |
|---|---|---|---|---|
| low | $0–$5 | $0.50 | 10 | Testing only — far too coarse for production |
| medium | $0–$20 | $0.10 | 200 | Common default — acceptable for general display |
| high | $0–$20 | $0.01 | 2000 | Premium display — risk hitting GAM key-value limits |
| auto | $0–$20 | variable | varies | Stepped: finer at lower CPMs, coarser at higher |
| dense | $0–$8 | $0.01–$0.05 | 400 | Video — finer granularity where bids cluster |
| custom | Any | Any per tier | Your choice | ★ Best — match your actual CPM distribution from reports |
gulp serve to start a local dev server.debug: true and capture the full auction log from the browser console.prebid-test-log.json — annotate bid, no-bid, and timeout events per adapter.priceGranularity: 'medium' and then with a custom 3-tier config.hb_pb key-value set for each winning bid under both configs.granularity-comparison.csv with: actual CPM, medium bucket, custom bucket, delta.pbjs.getBidResponses() 20 times across different times of day.bidderTimeout that captures 95% of bids without adding unnecessary page latency.timeout-audit.md with per-adapter timeout rate and recommended timeout value.Enter any two values
to calculate the third
More tools coming soon