The Prebid.js Price Floors module: a practical setup guide
A price floor in Prebid.js is two things: a signal to bidders about the price to beat, and a rule that throws out bids below it. The Price Floors module (priceFloors) does both, per ad unit, media type and size, and reports what it did to your analytics. Set it up with a fallback rule set, the currency module and a skipRate holdout, and align it with your Google Ad Manager pricing rules before you turn enforcement on.
Everything below is checked against the module docs and the current Prebid.js 11 source (11.44 at the time of writing).
What does the Price Floors module do?
Once priceFloors is in your build and floors is in setConfig, the module works at three points in each auction:
- Before bidding: it adds a
getFloor()function to each bid request. Adapters call it with a currency, media type and size, and pass the floor to their endpoint. Prebid Server receives it asimp.bidfloorandimp.bidfloorcur. - On each bid response: it finds the matching rule and compares the bid to the floor. A bid below the floor is rejected with the reason
Bid does not meet price floorand fires abidRejectedevent. - For analytics: it attaches
floorDatato bid requests and responses: the rule matched, the floor value, the rule location, the model version and whether the auction was skipped.
If you only want to send one static floor and never enforce it, Prebid says not to use this module. Set ortb2Imp.bidfloor and bidfloorcur on the ad unit instead.
Static or dynamic floors?
Rules can live in three places, called rule locations. Each auction uses exactly one, chosen in this order:
- Dynamic: fetched from
floors.endpoint.url(a GET returning JSON that is merged intodata). - setConfig:
floors.datain your page or package config. - Ad unit: a
floorsobject on each ad unit. This also needspbjs.setConfig({ floors: {} }), or nothing happens.
Prebid’s own advice is blunt: static floors “are blunt tools and you’ll forget to update them”. Most publishers should get to dynamic floors, either from a floor provider or from their own pipeline that writes a JSON file to a CDN.
Two settings matter for dynamic floors:
floors.auctionDelaydefaults to 0. With 0, Prebid does not wait for the fetch, so the first auctions on a page use yourdatafallback (or no floors at all). A value such as 100 ms waits up to that long for the file. The wait happens before bid requests go out, so it sits on top of your auction timeout.- Always keep
dataas a fallback. It is used whenever the fetch fails or arrives late. Your analytics will seefetchStatusassuccess,timeoutorerror.
The fields that matter
| Field | Default | What it does |
|---|---|---|
data.currency | 'USD' | Currency of the floor values. |
data.schema.fields | none | Rule dimensions: gptSlot, adUnitCode, mediaType, size, domain, plus custom fields via additionalSchemaFields (Prebid.js only). |
data.schema.delimiter | pipe character | Separator inside rule keys. |
data.values / data.default | none | Rule keys mapped to floors, and the floor when nothing matches. |
floorMin | none | Lower bound: the module uses the greater of floorMin and the matched rule. |
skipRate | 0 | Share of auctions (0 to 100) where all floor logic is skipped. |
data.floorsSchemaVersion / modelGroups | 1 | Version 2 allows several models, each with a required modelWeight, sampled per auction. |
enforcement.enforceJS | true | Reject bids below the floor in the browser. False still signals floors. |
enforcement.enforcePBS | true | Ask Prebid Server to enforce too. |
enforcement.floorDeals | false | Whether deal bids are held to the floor. |
enforcement.bidAdjustment | true | Adjust floors with each bidder’s bidCpmAdjustment. |
enforcement.enforceBidders | ["*"] | Which bidders client-side enforcement applies to. |
Three behaviours are worth knowing before you write rules:
- Matching is most specific first, weighted left to right. An exact value in the leftmost field beats a
there. Put your most important dimension first and always includecatch-alls, because many adapters ask for a floor withsize: '*'. gptSlotneeds GPT loaded first. The docs warn that using it before GPT loads can give unexpected results.bidAdjustmentworks on net prices. If a bidder bids gross and you applybidCpmAdjustment(say 0.85 to net it down), the module raises the floor it sends that bidder so the adjusted bid still has to clear your floor. Prebid also supports a custominverseBidAdjustmentinbidderSettings.
A working configuration
This is a page-level setup with a dynamic file, a fallback rule set, a global minimum and the currency module. The floor values are placeholders: replace them with numbers from your own bid data.
pbjs.setConfig({
currency: {
adServerCurrency: 'USD',
defaultRates: { USD: { INR: 85 } } // placeholder fallback rate; keep it current
},
floors: {
enforcement: { enforceJS: true, floorDeals: false, bidAdjustment: true },
floorMin: 0.05,
auctionDelay: 100,
endpoint: { url: 'https://cdn.example.com/floors/example-site.json' },
data: { // fallback if the fetch fails or is late
currency: 'USD',
skipRate: 10,
floorProvider: 'in-house',
modelVersion: 'fallback-v1',
schema: { delimiter: '|', fields: ['mediaType', 'size'] },
values: {
'banner|300x250': 0.40,
'banner|728x90': 0.30,
'banner|*': 0.25,
'video|*': 1.50
},
default: 0.20
}
}
});
The endpoint returns only what belongs under data. Here it is in Schema 2, testing two models against each other:
{
"currency": "USD",
"floorsSchemaVersion": 2,
"skipRate": 10,
"modelGroups": [
{
"modelWeight": 50,
"modelVersion": "slot-size-v3",
"schema": { "fields": ["gptSlot", "mediaType", "size"] },
"values": {
"/1111/home/top|banner|728x90": 0.45,
"/1111/home/top|banner|*": 0.35,
"*|banner|*": 0.25,
"*|video|*": 1.50
},
"default": 0.20
},
{
"modelWeight": 50,
"modelVersion": "mediatype-v1",
"schema": { "fields": ["mediaType"] },
"values": { "banner": 0.25, "video": 1.50 },
"default": 0.20
}
]
}
Weights are normalised, so 50/50 and 1/1 mean the same thing. Each auction picks one model, and analytics adapters receive modelVersion and modelWeight with every bid.
How do Prebid floors interact with GAM pricing rules?
They stack, and that is where many setups leak value.
Google Ad Manager’s pricing rules (long known as unified pricing rules) apply to Price Priority, Network and Bulk line items. Google states that these line items “must have a CPM that meets or exceeds the price set in pricing rules to compete in the auction”. Prebid line items are usually Price Priority, and their rate is the price bucket, not the exact bid.
That leads to three practical rules:
- Keep the two in step. If your Prebid floor for a slot is $0.30 and the GAM pricing rule for that inventory is $0.50, bids between the two still get
hb_pbtargeting, but their line items cannot compete. Prebid analytics will show them as top bids that never render. GAM’s line item troubleshooting lists line items below a rule’s price. - Remember bucket rounding. Prebid rounds bids down into buckets: the medium granularity, for example, uses $0.10 steps up to $20. A $1.08 bid lands in the $1.00 line item, so a $1.05 pricing rule blocks it even though the bid cleared $1.05.
- Compare like with like. GAM applies pricing rules to what the publisher would be paid. Prebid compares floors to the bid after
bidCpmAdjustment. If some bidders bid gross, net them down inbidderSettingsso both systems use the same prices.
A floored-out Prebid bid is not always lost revenue. AdX or another line item may fill the impression at a higher price. It is lost when nothing else fills, which is why fill and total revenue per pageview are the numbers to watch.
Common mistakes
- Wrong currency.
data.currencydefaults to USD. Floors written as rupee values without settingcurrency: 'INR'become dollar floors and kill fill. - No currency module. When the floor currency and bid currency differ and no conversion is available, the current code logs an error and lets the bid through unenforced.
getFloor()falls back to returning the floor in its own currency. Include the module, setdefaultRates, and check that bidders declare their bid currency. - Floors too high. Track the share of bids rejected with
Bid does not meet price floor, by bidder and slot. A sharp rise with no gain in revenue per pageview means the floors are above what the market pays. - Ad unit floors without
setConfig({ floors: {} }). The module never runs. - No catch-all rules. Adapters that ask for
size: '*'and auctions with no exact match fall back todefault, or get no floor at all. - Forgetting deals. Deal bids skip enforcement unless
floorDealsis true. That is usually what you want, but make it a deliberate choice.
A sensible rollout plan
- Baseline. Log bid prices by slot, size, media type, device and country with no floors, for at least a full weekly cycle.
- Signal only. Turn the module on with
enforceJS: false. Bidders receive floors but nothing is rejected in the browser. Watch bid rates and prices by bidder, since many SSPs act on the floor themselves. If you use Prebid Server, setenforcePBSto match, as it defaults to true. - Enforce with a large holdout. Switch on enforcement with a high
skipRate. Prebid’s docs describe starting as high as 90. Compare skipped and floored auctions (floorData.skipped) on total revenue per pageview, fill, and Prebid’s share of wins in GAM. - Shrink the holdout. Lower
skipRatein steps, but never to zero. A 5 to 10% holdout, the values Prebid’s examples use, keeps showing you whether the floors still earn their keep. - Iterate with model groups. Test new rule sets as a second
modelGroupsentry rather than replacing what works.
Checklist
priceFloorsandcurrencyare in the build.- A fallback
datarule set with*catch-alls and adefault. data.currencyset explicitly.floors.auctionDelaychosen on purpose and counted in your timeout budget.floorMinset as a safety net under dynamic floors.- Prebid floors aligned with GAM pricing rules, allowing for bucket rounding.
skipRateholdout running, with analytics readingfloorData.bidRejectedwithBid does not meet price floortracked by bidder and slot.
At BidCliq, floor reviews start from the same place every time: what share of bids is rejected, and whether total revenue moved. Neither number on its own tells you enough.
Frequently asked questions
What does the Prebid.js Price Floors module do?
It selects a floor for each ad unit, media type and size from your rules, passes it to bid adapters through getFloor(), and rejects bid responses that come in below it. It also attaches floor data to bids so analytics adapters can measure the effect.
What is skipRate in Prebid floors?
skipRate is a number from 0 to 100 that sets the share of auctions where all floor logic is skipped. 0 means floors always apply, 100 means they never apply, and the default is 0. It works as a built-in A/B holdout.
Do Prebid floors apply to deals?
Not by default. enforcement.floorDeals is false, so bids carrying a deal ID are not rejected for being under the floor unless you set it to true.
Do I need the currency module for price floors?
Yes, whenever your floor currency differs from bid or ad server currencies. Prebid's docs warn that leaving it out can cause unexpected behaviour, and in the current code a bid that cannot be converted passes through as if no floor matched.
How do Prebid floors interact with Google Ad Manager pricing rules?
They stack. Prebid removes bids below its floor before targeting is set, and GAM pricing rules stop Price Priority line items whose rate is below the rule price from competing. The stricter of the two decides what Prebid demand can win.

About the author
Founder, BidCliq
Sharad Yadav is the founder of BidCliq, a publisher-first ad monetization platform that helps publishers and ad ops teams run header bidding, Prebid and programmatic across display, video, app and CTV. Before BidCliq, he led AdTech and product at GeeksforGeeks, where he built an in-house ads product that doubled programmatic revenue within nine months and an anti-adblock solution that lifted ad yield by 10%. He is a founding member of AdCP.