VASTlint
Back to blog
Live events/9 min read

A Live Ad Break Opens on a Cue. VMAP Was Designed for a Schedule.

Live sports and event streaming break most of the assumptions VAST and VMAP were built on: the break is announced seconds before it starts, its length is fixed by the transport stream, every viewer hits it at once, and nothing can be retried. Where the creative side actually fails, and what to check before the next event window.

Author

Alex Sekowski

Published

July 29, 2026

Reading time

9 min read

LiveVMAPSSAIAd podsCTV

The break you cannot reschedule

Ad insertion for a live event is the same protocol stack as ad insertion for an on-demand episode, running under conditions it was not designed for. Three of those conditions do most of the damage.

The break is not on a timetable. It is announced by an SCTE-35 cue message inserted into the transport stream by the broadcast operation, usually with a short pre-roll interval before the splice point. Everything downstream, ad request, auction, VAST resolution, creative selection, transcode check and manifest manipulation, has to finish inside that interval.

The break has a fixed length that the stream is going to honour whether or not you fill it. Underfill is slate or dead air on a live feed with an audience watching. Overfill means a truncated ad or a late return to content, which on a sports feed means missing the restart.

And every viewer on the feed arrives at the same instant. On demand, a million viewers spread their mid-rolls across the day. Live, a million viewers hit the same cue inside the same second, so anything that is a slow leak in VOD becomes a spike with no recovery window.

None of that is a VAST problem in the sense of a spec defect. VAST and VMAP do their job. But several defaults that are harmless on demand become expensive live, and they are all checkable in advance.

VMAP describes a schedule. A live break has none.

VMAP exists to describe where ad breaks sit inside a piece of content: a preroll, a midroll at eleven minutes, a postroll. Every AdBreak carries a required timeOffset attribute telling the player when to insert. That model assumes the breaks are known before playback starts.

For a live feed they are not. The producer decides. The cue arrives. So the working pattern is not a static VMAP served once at stream start, it is a VMAP document generated per cue, or the stitcher calling the ad server directly on cue receipt with the break parameters attached to the request. The static-playlist mental model is where a lot of live integrations go wrong, because it is the one every VMAP tutorial teaches.

This is what a per-cue break looks like. The timeOffset is the real presentation timestamp of the splice point, computed from the cue, and the break identity travels in breakId so the tracking can be reconciled against the broadcast log afterwards.

One AdBreak, generated when the cue arrives
xml
<?xml version="1.0" encoding="UTF-8"?><vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap" version="1.0">  <vmap:AdBreak timeOffset="00:41:12.480" breakType="linear" breakId="cue-88421">    <vmap:AdSource id="q3-break-2" allowMultipleAds="true" followRedirects="true">      <vmap:AdTagURI templateType="vast4">        <![CDATA[https://ads.example.com/live?pod=q3-break-2&dur=120&cb=[CACHEBUSTING]]]>      </vmap:AdTagURI>    </vmap:AdSource>    <vmap:TrackingEvents>      <vmap:Tracking event="breakStart"><![CDATA[https://t.example.com/break?e=start&id=cue-88421]]></vmap:Tracking>      <vmap:Tracking event="breakEnd"><![CDATA[https://t.example.com/break?e=end&id=cue-88421]]></vmap:Tracking>    </vmap:TrackingEvents>  </vmap:AdBreak></vmap:VMAP>

timeOffset has five legal forms, and none of them mean "now"

The legal values are a timecode of the form hh:mm:ss or hh:mm:ss.mmm, a percentage, the keyword start, the keyword end, and a position marker of the form #m. That is the whole vocabulary. There is no value that means "whenever the cue fires," which is exactly why the document has to be generated with a real timecode in it rather than templated once with a placeholder.

Two things go wrong here often enough to be worth naming. The first is a placeholder that never got substituted, which produces a value the player cannot parse and a break that never opens. The second is repeatAfter on a break whose timeOffset is start or end, which is a copy-paste from a VOD template: those positions occur exactly once per playback, so the repeat has no effect and the mid-stream breaks the author thought they had scheduled do not exist.

vastlint reports the first as VMAP-1.0-adbreak-timeoffset-format at error severity and the second as VMAP-1.0-repeatafter-conflict at warning severity. Both are static checks. Neither requires the event to be running.

The wrapper chain is spending a budget it did not set

A VAST wrapper is a redirect. Each hop in the chain is a full network round trip from the resolving party to another ad server, and only after the last hop returns an InLine response does anyone have a media file to fetch.

In on-demand playback that latency is absorbed. The player has buffer, the viewer is not going anywhere, and a chain of four or five hops resolves in time that nobody notices. In a live break the same chain is spending the cue pre-roll interval, which was not allocated for it and is shared with the transcode and manifest work that has to happen after the creative is chosen.

vastlint's default maximum chain depth is five, which reflects what players and SSPs commonly enforce. That default is a spec-compliance ceiling, not a live-operations target. A chain that resolves reliably in a VOD test harness can exhaust a live decisioning window entirely, and the outcome is not a slow ad, it is no ad, because the stitcher has to splice something at the cue point whether or not the auction finished.

The practical move before an event is to measure the chain depth and resolution time of every tag in the rotation, not to assume the number is small. Wrapper chains grow through intermediaries without anyone editing a tag.

The error codes that describe a live failure after the fact

  • 301: wrapper timeout or wrapper resolution failure. In live this is the signature failure. The chain did not return before the window closed.
  • 302: wrapper limit reached. The chain was longer than the resolver's configured maximum, which is often lower on a live path than on a VOD path.
  • 303: no ads after wrappers. Often a legitimate no-fill, but on a live break it still costs you a filled slot, so it belongs in the same report as the errors.
  • 401 and 402: media file not found, and media file timeout. On a live splice the CDN is being asked for the same asset by every stitcher session at once, which is precisely when a cold object is expensive.
  • 403 and 405: unsupported media, and a problem displaying a supported media file. These are the codec and container mismatches that a stitcher discovers at splice time rather than at trafficking time.

Duration is a contract with the transport stream

The Duration element inside Linear is required, and in VOD it functions mostly as a hint: the player uses it for the countdown, the skip timer and companion scheduling, and if the media file runs a second longer than declared nothing catastrophic happens.

In a live break it is a contract. The stitcher is assembling a fixed-length segment of stream from the durations it was told, and it is doing that before it has watched the file. If the declared Duration and the actual media length disagree, the assembled break does not line up with the splice point. Short means dead air at the end of the pod. Long means the return to content is late, or the last ad is cut off, which also means its complete event never fires and it is not billable.

On the buy side this is why OpenRTB grew a field for exact acceptable durations rather than a range. The rqddurs array in the bid request says the seller will take a 15 or a 30 and nothing in between, and the spec text names the reason directly: the live TV use case, where non-exact durations produce dead air. A creative whose VAST declares 30 and whose file runs 28 satisfies the auction and breaks the stream.

Which makes the declared Duration one of the few VAST values worth verifying against the actual asset rather than trusting, particularly for creatives that were trafficked for on-demand campaigns and later swept into a live rotation.

On demand, a bad tag is a slow leak you can investigate tomorrow. On a live break it is one hundred percent of two minutes of inventory, and there is no tomorrow for that audience.

vastlint field note

Get VAST spec updates, platform guides, and release notes in your inbox.

There is no second request

A live pod is a sequence of ads delivered against one break with a hard end time. If the ad in slot two fails to resolve, there is no realistic path to requesting a replacement, because the time that would take is time the break does not have. The pod either has a fallback already in hand or it plays slate.

That raises the cost of ordering ambiguity. When some Ad elements in a response carry a sequence attribute and others do not, playback order is undefined, and the player or stitcher resolves it however it resolves it. In VOD that produces an occasional out-of-order pod that nobody notices. In a live break with per-slot pricing and competitive separation rules, an undefined order can put two competing advertisers back to back on a national feed.

vastlint reports mixed sequencing as VAST-2.0-ad-sequence at warning severity. It is a warning because the tag still serves. Treat it as an error in a live rotation.

Running a pod with a typo'd macro and mixed sequencing through the validator produces the findings below. Every one of these is static, and every one of these would be a live failure discovered during the event.

vastlint findings on a two-slot live pod
text
error    VAST-4.1-adservingid-present       InLine is missing AdServingId (x2)error    VAST-4.0-universaladid-present     Creative is missing UniversalAdId (x2)warning  VAST-2.0-ad-sequence               ordering across the pod is ambiguouswarning  VAST-2.0-linear-tracking-quartiles no quartile tracking events (x2)warning  VAST-2.0-macro-unknown             [CACHEBUSTNG] is not an IAB macroinfo     VAST-4.1-mezzanine-recommended     no Mezzanine; stitchers may reject (x2)info     VAST-2.0-adsystem-no-version       AdSystem has no version attribute (x2)

Every viewer arrives in the same second

The macro finding in that list is the one whose cost scales with the audience. A cachebusting macro with a typo in it is never substituted, so the literal token stays in the URL and every request from every device is byte-identical.

On demand, identical tracking URLs arriving over hours mostly still get counted, because they are separated in time and by IP. On a live break they arrive as one burst of identical requests to the same endpoint, which is the exact traffic shape that caches, proxies and dedupe logic are built to collapse. The impressions do not fail loudly. They are absorbed, and the discrepancy surfaces days later as a reconciliation argument with no evidence trail.

The same reasoning applies to any macro the pipeline is supposed to expand and does not. vastlint reports unrecognised tokens as VAST-2.0-macro-unknown, lowercase macro names as VAST-2.0-macro-lowercase, and unencoded macros inside a URI parameter as VAST-2.0-macro-uri-unencoded. In a live context these stop being hygiene findings and become measurement findings.

Worth noting that server-side insertion changes who expands the macro in the first place. In a fully server-side path the beacons fire from the stitcher's data centre, so the IP and user agent on the tracker belong to the stitcher rather than the viewer, and any macro the player would normally have filled has to be filled by the stitcher instead.

The mezzanine finding is not decoration

The Mezzanine element carries a high-quality source file intended for transcoding rather than direct playback, and vastlint reports its absence at info severity with the note that ad-stitching servers may reject the tag in CTV and SSAI contexts.

For client-side VOD that severity is right. For a live stitched break it understates the risk. A stitcher needs a rendition that matches the encoding profile of the content stream so the splice is seamless, and if the tag offers only renditions that do not match, the platform either transcodes at insertion time, which costs part of the window, or declines the creative. Neither outcome shows up as a VAST error. It shows up as a slot that filled with something else.

Live is the case where trafficking a mezzanine is worth the storage, and where the info-level finding deserves to be promoted to a gate in whatever check runs before a rotation goes live.

Before the next live window

  • Validate the generated VMAP, not the template. The bug is almost always an unsubstituted timeOffset or a repeatAfter inherited from a VOD playlist, and both are invisible in the template.
  • Measure wrapper chain depth and resolution time for every tag in the live rotation. The compliance ceiling is five hops; the live budget is whatever the cue pre-roll interval leaves after transcode and manifest work.
  • Verify declared Duration against the actual media length for any creative entering a live pod, especially creatives promoted from an on-demand campaign.
  • Require a Mezzanine, or confirm with the stitching platform that your renditions match its encoding profile. Do this before the event, not from the incident channel during it.
  • Treat mixed sequencing across the pod as a blocking error rather than a warning, because undefined ordering plus competitive separation rules is a client conversation.
  • Sweep every URL in the rotation for unknown, lowercase and unencoded macros. Burst traffic is where deduplication eats identical requests.
  • Validate what the stitcher emits, not only what you author. The tag that runs during the break has been rewritten at least once on its way there.

Check a live rotation before the cue fires

Paste a VAST tag or a generated VMAP document into the validator and it reports the wrapper, duration, macro, sequencing and mezzanine findings with the spec reference and fix guidance for each. It runs locally in the browser and nothing is stored.

Open the validator

Sources and further reading

The AdBreak, AdSource and TrackingEvents definitions quoted here, including the legal timeOffset forms.

The cue message that opens a live break, and the source of the pre-roll interval the whole ad decisioning path has to fit inside.

The companion piece on what a stitcher does to a tag between authoring and playback, and where to validate in that pipeline.

Every VMAP rule vastlint checks, with severities and examples.

How chains grow, what enforces the ceiling, and how to measure resolution time.

The 301, 302, 303, 401, 402, 403 and 405 codes referenced above, with the causes behind each.

The buy-side half of the same break: which OpenRTB fields describe live content, why rqddurs exists, and the field that changed meaning in June 2026.

Keep reading

Related stories

All posts