Guides
OpenRTB bid request best practices
Most of what makes a bid request good is not in the field list; it is in the transport around it and the discipline behind it. This is the practices side of common mistakes: how to send a clean, cheap, verifiable OpenRTB 2.6 request, with the reason and the spec section behind each rule. Written for the exchange or SSP building the request, though bidders reading one will recognize what good looks like.
1. Get the transport right before the JSON
The single highest-leverage change is not a field. The spec calls it out in Section 2.1 as its one bolded best practice: use HTTP persistent connections, also known as Keep-Alive. Bid requests are POSTed at high frequency to a stable set of bidders, so reusing connections "has a profound impact on overall performance by reducing connection management overhead as well as CPU utilization on both sides of the interface." Opening a fresh TCP and TLS handshake per auction, at real QPS, is the most expensive mistake you can make and it never shows up in a validator.
Alongside it, two more transport rules from Sections 2.1 and 2.2:
- POST, and honest status codes. Bid requests must be HTTP POST. Return 200 for a response with content, 204 for no content (one valid way to signal a no-bid), and 400 for a malformed payload. Mapping a parse failure to 200 hides bugs on both sides.
- HTTPS only. HTTPS is not technically required by the protocol, but the spec is blunt: it is strongly recommended that exchanges and bidders use only HTTPS. There is no reason to run a bid stream in the clear in 2026.
2. Compress in both directions
Section 2.4 is explicit that compression should be enabled on the bid request as well as the response, and that "even binary data formats can benefit from being compressed." The response side is easy: the exchange sends Accept-Encoding: gzip and a correctly configured bidder replies with Content-Encoding: gzip. The request side is the one teams forget, because it has to be agreed in advance: the exchange sets Content-Encoding: gzip on the request itself, and the bidder has to know to expect it.
# response compression (standard, always worth enabling) accept-encoding: gzip # exchange -> bidder, on the request content-encoding: gzip # bidder -> exchange, on the response # request compression (must be pre-agreed with the bidder) content-encoding: gzip # exchange -> bidder, on the request body
On real bid streams the request body is where the bytes are, so request-side gzip is often the larger saving. It also pairs with the payload discipline in rule 8: a smaller, well-scoped request compresses better than a bloated one.
3. Always send the version header
Section 2.5 asks exchanges to send the protocol version in an HTTP header so a bidder can branch before it parses:
x-openrtb-version: 2.6
It costs nothing and it saves a bidder from guessing. Note that the header carries only major.minor (2.6), not the dated snapshot. Since 2.6-202211 the version number only moves on breaking changes, so 2.6 covers every snapshot from 202211 through 202606. Which snapshot you actually emit is a matter for your partner docs and your validation config, not the wire.
4. Treat new fields as additive and never reject unknowns
The 2.6 versioning model (Section 2.6) says an exchange should start transmitting a new field as soon as it implements support, and a bidder consumes it at its discretion. Nobody negotiates an "upgrade." The practical consequence sits on the reading side: your parser must tolerate attributes it does not recognize. A JSON schema set to additionalProperties: false will reject a request the moment a new snapshot adds a field, or worse, drop it silently. That is exactly how a bidder stops seeing content.data[].cids or the new content.realtime without a single error in the log. Validate against the spec, not against a frozen schema that treats the future as invalid.
5. Make the required core correct and complete
A bid request requires exactly two things at the top level: id and imp, an array with at least one impression. Each Imp needs its own id, unique within the request, and exactly one media type object among banner, video, audio, or native to say what is for sale. Two small type traps that drop bids without erroring:
at(auction type) is an integer, not a string."1"is wrong;1is right.- Send
siteorappordooh, never more than one. A request with bothsiteandappis contradictory and some buyers reject it outright.
The full object-by-object list, and which fields are required versus recommended, is in the bid request reference.
6. Put privacy signals in their first-class 2.6 paths
Every regulatory signal moved out of ext and got a real home in 2.6. Sending the old extension paths is the most common way to have consent silently ignored by a buyer that reads only the current ones:
regs.gdpranduser.consent, notregs.ext.gdpr/user.ext.consent.regs.gppwithregs.gpp_sid. Send the GPP string and its section IDs together; one without the other is not actionable.regs.us_privacyandregs.coppawhere they apply.
Exact paths, versions, and the DSA fields are in the privacy signals reference. Getting these wrong is not a dropped bid, it is a compliance exposure.
7. Carry a complete, well-formed supply chain
Buyers increasingly gate spend on a clean source.schain. In 2.6 the object lives at source.schain, not the old source.ext.schain. Two things buyers actually check:
complete: 1means every hop from publisher to exchange is represented. Set it truthfully; acomplete: 1chain with a gap is worse than an honestcomplete: 0.- Each node needs a real
asiandsid, no empty identifiers and no duplicate adjacent nodes.
The schain does not stand alone. It should reconcile with your ads.txt / app-ads.txt and your sellers.json, and those four together are what a careful buyer verifies. Node-by-node detail is in the supply chain reference.
8. Price and time the auction honestly
- Always pair the floor with its currency. Send
imp.bidfloorandimp.bidfloorcurtogether. If you omitbidfloorcurit defaults toUSD, which silently turns a floor expressed in another currency into the wrong number. See the bid floors reference. - Set a realistic
tmax. It is the time the exchange will wait for a response. Too low and honest bidders time out and look like no-bids; a non-positive or absurd value is just a bug. The tmax reference covers whether it includes network time and typical values.
9. Send less, but not too little
Bid request size is a direct cost: bandwidth on every auction, parse time on every bidder, and QPS multiplied across a fanned-out stream. The discipline is to send what a buyer can act on and trim what it cannot:
- Drop deprecated and removed fields rather than dual-sending them forever. The enum reference notes where AdCOM lists took over.
- Do not pad arrays "just in case." An empty
imp.pmp.dealsor adataarray full of blank segments is bytes with no signal. - But do not strip signals a buyer prices on. The goal is a lean request, not a starved one: identity, privacy, and supply chain earn their bytes.
Remember the spec convention from Section 2.3: the absence of an attribute has a formal meaning, usually "unknown." Omitting a field is a statement, so omit deliberately.
10. Validate continuously, not once
Almost none of the above throws an error at runtime. A stale placement enum, a floor in the wrong currency, a consent string in ext: they all parse fine and just cost bids from whichever partner enforces the rule. The only way to catch them is to check requests against the spec on an ongoing basis.
- Spot-check a request in the browser tester, entirely client-side.
- Gate builds and sample live traffic with the CLI:
rtblint validate request.json, with--format jsonfor machine-readable output. - Wire it into CI so a fixture that drifts from the spec fails a build, not a campaign. See OpenRTB validation in CI.