Blog · CTV

Live event ad insertion: a burst, not a stream, and the OpenRTB fields that carry it

On-demand video produces a stream of impression opportunities. Each viewer starts when they start, hits their break when they hit it, and the request volume smooths out. A live event does not work that way. A cue message goes out in the transport stream, every viewer on the feed reaches the same break inside the same few seconds, and the auction has to clear before the splice point arrives. The shape of the demand is a burst, and the deadline is set by broadcast equipment rather than by an exchange. OpenRTB has acquired real vocabulary for this over the last four years. Most live inventory still ships without it.

What is different about a live break, in protocol terms

Three things, and each one lands on different fields.

The break opens on a cue, not a timestamp. In a live stream the ad opportunity is announced by an SCTE-35 message inserted into the transport stream by the broadcaster, usually with a short pre-roll interval before the splice point. The ad server does not know the break exists until that message arrives, and the entire decisioning path (request, auction, response, creative selection, manifest manipulation) has to complete before the splice. Nothing in OpenRTB models the cue. What OpenRTB controls is the slice of that interval the auction is allowed to consume, which is tmax.

The break has a fixed length and it must be filled exactly. A linear break is a hole of a known number of seconds in a stream that is going to continue regardless. Underfill is dead air or slate. Overfill is a truncated ad or a missed return to content. On demand, a 28-second creative in a 30-second slot is a rounding error. Live, it is two seconds of black.

Insertion is almost always server-side. Client-side ad insertion at live scale means every device independently fetching and buffering a creative at the same instant. The practical answer is server-side stitching, which changes what the buyer can see about the device and what can be measured. There is a field for declaring that, and it is the most under-populated field in this entire post.

Three integers describe live, and one of them changed meaning

The Content object carries the live signals. As of the 2.6-202606 snapshot there are three, and they are deliberately orthogonal.

content.livestream      0 = broadcast not scheduled (VOD, user initiated)
                        1 = broadcast is scheduled (linear viewing)

content.realtime        0 = not happening in real time (a replay)
                        1 = happening in real time (a live game)

content.firstbroadcast  0 = not the first time this content is broadcast
                        1 = first time being broadcast

The combinations are the point. A live semi-final is 1, 1, 1. A scheduled rebroadcast of that same match the next morning is 1, 0, 0: still linear, still a scheduled slot, but nothing is happening in real time and the outcome is already known. A FAST channel airing a library episode is 1, 0, 0 as well. A viewer pulling up the match highlights on demand is 0, 0, 0. Those are four genuinely different products and until June 2026 the protocol collapsed them into one boolean.

That is the part worth flagging. In 2.6-202505 and earlier, livestream was defined as "0 = not live, 1 = content is live (e.g., stream, live blog)." In 2.6-202606 the same field, same name, same type, means "is the broadcast scheduled." The live-ness moved to realtime. A sender implemented against the old text and a sender implemented against the new text both emit livestream: 1 and mean different things, and no receiver can distinguish them from the payload alone. This is the failure mode we wrote about in the version mismatch post, arriving on a field that buyers are about to start pricing against.

The corollary is that sending the new fields to a partner still on an older snapshot is not a soft degradation. Validating the same payload against 2.6-202505 produces:

openrtb.field.undefined  error  app.content.realtime
    Content.realtime is not defined in the canonical OpenRTB 2.6-202505 catalog.
openrtb.field.undefined  error  app.content.firstbroadcast
    Content.firstbroadcast is not defined in the canonical OpenRTB 2.6-202505 catalog.

In production nobody raises that error, because unknown fields are ignored by design. The fields are simply dropped, and the buyer prices real-time live inventory as if it were a replay. Knowing which snapshot each partner parses is the whole game, and it is covered field by field in the 202606 review.

imp.ssai: the field live inventory most needs and least sends

imp.ssai has been in the spec since 2.6 and is a four-value enum, not a boolean, and the distinction it draws is exactly the one a live buyer cares about:

0  status unknown (the default, and what most requests effectively say)
1  all client-side
2  assets stitched server-side, tracking pixels fired client-side
3  all server-side

The difference between 2 and 3 decides whether client-side measurement is possible at all. Under 3, impression and quartile beacons originate from a data center, so the IP and user agent on the tracker belong to the stitcher rather than the viewer, and anything that depends on executing code in the player (OMID viewability, interactive creative) is not going to run. Under 2, the assets are stitched but the beacons still fire from the device, which preserves most measurement.

A default of 0 means "unknown," and a buyer reading 0 has no basis to discount, no basis to expect measurement, and no basis to complain when it does not arrive. If your live supply is stitched, say so. It costs one integer, and the downstream consequences are covered from the creative side in vastlint's write-up on stitch-time failures.

The clock: tmax, imp.exp, bid.exp

tmax is the maximum time in milliseconds the exchange allows for bids, including internet latency. In live, that number is not a tuning preference. It is whatever is left of the cue pre-roll interval after transcode, manifest manipulation and CDN propagation have taken their share, and it is usually the tightest tmax a bidder will see from that seller. RTBlint flags openrtb.request.tmax_non_positive and openrtb.request.tmax_implausible because a zero or absurd value here is a config error that silently converts to "no bids arrived in time." The tmax reference covers the budget arithmetic.

The two expiry fields are the ones that get skipped. imp.exp is advisory as to the number of seconds that may elapse between the auction and the actual impression. bid.exp is the bidder's side of the same statement: how long it is willing to wait between winning and rendering. Live stacks frequently decision a break ahead of the splice point, and sometimes decision several breaks ahead during a predictable window. Every second of that gap is a second in which a frequency cap goes stale, a budget is spent elsewhere, or a creative is pulled. A request that prefetches and sends no imp.exp is telling the bidder the impression happens immediately, and it does not.

What a live pod request actually looks like

Pod bidding shipped in 2.6 in 2022 and the live case is why several of the fields exist. This is a single impression representing a dynamic pod: a two-minute break, up to four ads, only 15 and 30 second creatives accepted, with a per-duration floor. It validates clean against 2.6-202606.

{
  "id": "live-break-8842",
  "tmax": 220,
  "at": 1,
  "cur": ["USD"],
  "imp": [{
    "id": "1",
    "ssai": 3,
    "exp": 45,
    "video": {
      "mimes": ["video/mp4"],
      "protocols": [7, 8],
      "w": 1920, "h": 1080,
      "plcmt": 1,
      "linearity": 1,
      "podid": "q3-break-2",
      "podseq": 2,
      "poddur": 120,
      "maxseq": 4,
      "rqddurs": [15, 30],
      "slotinpod": 0,
      "mincpmpersec": 0.45,
      "durfloors": [
        { "mindur": 15, "maxdur": 15, "bidfloor": 22.0 },
        { "mindur": 30, "maxdur": 30, "bidfloor": 40.0 }
      ]
    }
  }],
  "app": {
    "id": "app-1",
    "bundle": "com.example.sports",
    "content": {
      "id": "match-5521",
      "livestream": 1,
      "realtime": 1,
      "firstbroadcast": 1,
      "cattax": 7,
      "network": { "id": "net-9", "name": "Example Sports Network" },
      "channel": { "id": "ch-2", "name": "Example Sports 2" }
    }
  },
  "device": {
    "devicetype": 3,
    "ifa": "38400000-8cf0-11bd-b23e-10b96e40000d",
    "lmt": 0
  },
  "source": { "fd": 1, "tid": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9" }
}

podid groups impression opportunities that belong to the same break. podseq is which break this is within the stream, which for a sporting event maps to the quarter or the innings break and is the single most useful targeting signal in the payload. poddur is the length of the whole break; rqddurs constrains the individual slots. slotinpod at 0 means any position. The full mechanics are in the pod bidding post and the CTV reference.

rqddurs exists because of dead air

Of all the pod fields, rqddurs is the one written specifically for this use case, and the spec says so in the field definition: it "specifically targets the Live TV use case where non-exact ad durations would result in undesirable dead air." It is an array of exact acceptable durations. Not a range. A 22-second creative is not a near miss against [15, 30], it is a rejection.

It is also mutually exclusive with minduration and maxduration, and the most common way to break a live pod request is to leave the old duration fields in place while adding the new one. That is a real check, not a style preference:

openrtb.fields.mutually_exclusive  error  imp[0].video.minduration
    imp[0].video.minduration and imp[0].video.rqddurs are mutually exclusive
    and must not both be present.
openrtb.fields.mutually_exclusive  error  imp[0].video.maxduration
    imp[0].video.maxduration and imp[0].video.rqddurs are mutually exclusive
    and must not both be present.

A bidder that reads maxduration and ignores rqddurs will cheerfully return a 22-second ad for a slot that cannot hold it. Two warnings catch the other common shapes: openrtb.video.pod.rqddurs_empty when the array is present but empty, which is the classic template-with-no-values bug, and openrtb.video.pod.mincpmpersec_without_pod_context when a per-second floor appears without poddur or maxseq, which usually means a dynamic-pod snippet was copied onto a single-slot request.

Burst traffic is the cheapest thing you will ever validate

The reason to care about all of this before an event rather than during one is that live failures are not distributed the way on-demand failures are.

On demand, a malformed field is a slow leak. It affects whatever share of traffic comes from the broken path, it shows up as a slightly depressed fill rate, and there is always more traffic tomorrow to investigate with. In live, the same malformed field affects one hundred percent of a break, the break lasts two minutes, and there is no tomorrow for that inventory. The audience for a final does not come back on Tuesday.

The flip side is that this makes the failures easy to find, because they are systematic rather than random. Every request in a break comes from the same template, the same ad server build, the same integration. If rqddurs and maxduration are both present in one request, they are both present in all of them. One sample payload from a rehearsal stream tells you what the entire event will do. Paste it into the bid request tester, or gate it in CI following the validation in CI guide.

The pre-event checklist

  • Confirm which snapshot each demand partner parses. If they are on 2.6-202505 or earlier, realtime and firstbroadcast are being dropped on the floor and your live premium is invisible. Send livestream knowing they read the old definition.
  • Populate imp.ssai honestly. 3 if beacons fire server-side, 2 if only the assets are stitched. Leaving it at 0 asks the buyer to guess, and buyers guess conservatively.
  • Check tmax against the actual cue pre-roll interval, not against your on-demand default. If the auction budget exceeds what the splice point allows, every bid is late and the break fills with house ads.
  • Send imp.exp if you decision ahead of the splice. Any non-trivial gap between auction and playout is information the bidder needs to price and cap correctly.
  • Validate one rehearsal payload end to end. Duration field exclusivity, enum ranges, pod coherence. These are template bugs, and a template bug found once is fixed for the whole event. The rule catalog lists what gets checked.
  • Validate the creative side too. A clean bid request that wins a slot and then returns a VAST tag the stitcher cannot use is the same lost break. The handoff is covered in VAST and OpenRTB.

Sources

The OpenRTB specification is published by IAB Technology Laboratory under a Creative Commons Attribution 3.0 License. Field names, enumerated values and quoted field definitions are drawn from it; the analysis is our own. Diagnostic output shown here was produced by RTBlint against the 2.6-202606 and 2.6-202505 catalogs. RTBlint is independent and not affiliated with IAB or IAB Tech Lab.