A publisher tells you the pre-roll break is 90 seconds and takes four ads. Your ad server builds a pod of four. Reporting shows two impressions. You pull the VAST response, run it through a validator, and it comes back clean.
Nothing is broken in the sense a validator understands. The document is well-formed, schema-valid, and complete. It just describes two ads instead of four, and there is nothing in the XML that says it was ever supposed to describe four.
VAST has no pod container
This is the part that surprises people who have only read about ad pods rather than parsed them.
An ad pod in VAST is not an element. There is no <AdPod>, no maxAds, no maxDuration. A pod is a convention: several sibling <Ad> elements under <VAST>, each carrying a sequence attribute.
You can check this yourself against the schemas IAB Tech Lab publishes:
$ grep -c 'AdPod\|maxAds\|maxDuration' vast_2.0.1.xsd vast_4.0.xsd \
vast_4.1.xsd vast_4.2.xsd vast_4.4.xsd
vast_2.0.1.xsd:0
vast_4.0.xsd:0
vast_4.1.xsd:0
vast_4.2.xsd:0
vast_4.4.xsd:0
Zero, from 2.0 through the 4.4 draft. (4.3 is absent from that list because IAB Tech Lab never published a 4.3 XSD. The file in the repo is a one-byte placeholder, which is its own interesting problem and not this article's.)
The only pod-related construct in the schema is one optional attribute on <Ad>:
<xs:attribute name="sequence" type="xs:integer" use="optional">
<xs:annotation>
<xs:documentation>
Identifies the sequence of multiple Ads that are part of an Ad Pod.
</xs:documentation>
</xs:annotation>
</xs:attribute>
That is the whole feature. An integer, optional, with no relationship to any other ad in the document expressed anywhere.
I mention this because a fair amount of writing on ad pods, including a page on my own site until I checked it this week, describes an <AdPod> element with maxAds and maxDuration children. That element does not exist in any released VAST version. The names are real, but they come from the request side, and mixing them up sends people looking for a validation error that no validator can produce.
Pod size lives in the request, not the response
If you want to know how many ads a break should hold, you have to look at what was asked for, not what came back.
In OpenRTB 2.6 that is on the impression object:
{
"imp": [{
"video": {
"podid": "midroll-1",
"poddur": 90,
"maxseq": 4,
"podseq": 0,
"mincpmpersec": 0.05
}
}]
}
poddur is the total seconds available. maxseq is the maximum number of ads. In VMAP it is the <AdBreak> that owns the slot, with allowMultipleAds sitting on its <AdSource>.
The VAST response then fills the slot and carries none of that context forward. So when you hold a VAST document in your hand, you are holding something that cannot tell you whether it is complete. A two-ad pod and a truncated four-ad pod are byte-for-byte the same kind of object.
That asymmetry is the whole problem. Validation answers "is this document legal." It cannot answer "is this document all of what was sent," because the document does not carry its own expected size.
The four ways ads disappear
1. A wrapper collapses the pod
The most common one. Your pod passes through an SSP wrapper, and that wrapper sets:
<Wrapper allowMultipleAds="false" followAdditionalWrappers="true">
The player keeps the first ad and discards the rest. One impression where you expected four.
The subtler version is a wrapper that omits the attribute entirely. Here is the declaration in the 4.2 XSD:
<xs:attribute name="allowMultipleAds" type="xs:boolean">
<xs:annotation>
<xs:documentation>
a Boolean value that identifies whether multiple ads are allowed
in the requested VAST response.
</xs:documentation>
</xs:annotation>
</xs:attribute>
Note what is not there: no default. The schema does not say what an absent allowMultipleAds means. Some players read the absence as permission, some as refusal, and both readings are schema-valid. You will see the same tag pod correctly on one device and collapse on another, with no error on either.
Set it explicitly on every wrapper you control. Absence is not a default, it is a coin flip.
2. Sequence collisions and gaps
sequence is typed xs:integer, not xs:positiveInteger. That means all of these are schema-valid:
<Ad id="a" sequence="0">
<Ad id="b" sequence="-1">
<Ad id="c" sequence="2">
<Ad id="d" sequence="2">
Two ads with sequence="2" give the player no defined order between them. Most implementations fall back to document order, some drop one, and a few reorder unpredictably across sessions. Negative and zero values are accepted by the schema and handled inconsistently in the wild.
Mixing sequenced and unsequenced ads in one response is its own trap. An <Ad> with no sequence is a standalone ad, not pod member zero. A response containing both is asking the player to decide whether it received one pod or a pod plus a loose ad, and implementations split on the answer.
3. Duration overflow
The publisher owns the break length. If your four ads sum to 100 seconds and the break is 90, something has to give, and the player decides what. Usually it plays until the break is exhausted and drops the remainder, which means your last ad in sequence order is the one that silently never runs.
Nothing in the VAST document flags this, because the document has no idea what the break length is. The only place the mismatch is visible is by comparing poddur from the request against the sum of <Duration> values in the response, and that requires holding both at once.
Worth knowing: the VAST error code registry has a code for exactly this outcome. Code 206, introduced in VAST 4.1, reads "Ad Break shortened. Ad was not served." If your player emits 206, it is telling you the break ran out before your ad did. It is one of the more useful codes in the registry and one of the least commonly implemented, so treat its absence as no evidence either way.
4. One ad fails and takes the rest with it
A broken media file or an unreachable wrapper endpoint in the middle of a pod should cost you one ad. In several player implementations it costs you the remainder of the pod, because the error handling unwinds the whole break rather than skipping to the next sequence.
This is not spec-mandated behaviour, it is an implementation choice, and it is the reason pod position matters commercially. An ad in sequence 1 that fails is much more expensive than the same failure in sequence 4.
What you can actually check before it ships
Statically, from the response alone, you can catch:
- Duplicate
sequencevalues across<Ad>elements - Mixed sequenced and unsequenced ads in one response
- Zero and negative sequence values
- Wrappers in the chain that set
allowMultipleAds="false"or omit it - Per-ad validity, so that no single ad in the pod is the one that unwinds the break
The mixed-sequence case is the one I see most in real tags, and it has a rule ID: VAST-2.0-ad-sequence. The others fall out of ordinary per-ad validation, which matters more in a pod than anywhere else precisely because of failure mode 4.
What you cannot check statically is whether the pod is complete. That comparison needs the request. If you are debugging a short break, get poddur and maxseq from the bid request and put them next to the response before you start reading XML. Half the pod investigations I have watched went thirty minutes deep into a document that was never going to contain the answer.
Getting hands on it
I maintain vastlint, an open source VAST validator written in Rust, which is where the rule IDs above come from. Two things on the site are useful for pods specifically:
- The VAST tag tester fetches a live tag URL, follows the wrapper chain, and shows you the resolved response. That is how you find the wrapper that set
allowMultipleAds="false", since it will not be in the tag you started with. - The ad pods guide has the full sequenced-pod example and the checklist.
There is also a CLI if you would rather keep this in CI than in a browser:
$ cargo install vastlint
$ vastlint check pod.xml
The short version
A VAST response describes what was returned. It does not describe what was requested, and a pod is defined entirely by what was requested. Any tool that only reads the response is structurally unable to tell you an ad is missing, which is why "the tag validates" and "the break is short" are statements that can both be true at once.
Check the response for the things it can prove, and go to the request for the thing it cannot.
Top comments (0)