The hard thresholds
IAB's Display Creative Guidelines (most recent substantive update pre-2026) give you a single set of numbers every publisher treats as the floor. Miss any one of them and the bundle will either be rejected at upload or throttled in delivery.
| Check | Threshold | Measured how |
|---|---|---|
| Initial load weight | ≤ 150 KB | Sum of all bytes transferred before load event |
| Polite load (total) | ≤ 300 KB (display) / 1 MB (expandable) | Sum of all bytes over the creative lifetime |
| Load time | ≤ 4 s to fully rendered | Time from first byte to load |
| HTTP requests | ≤ 15 (display) / ≤ 30 (rich media) | Total requests including images, scripts, fonts, pixels |
| Script count | ≤ 5 | Count of <script> tags loaded after first paint |
| CPU usage | < 30% sustained | Chrome DevTools Performance panel |
| Frame rate | ≥ 24 fps during animation | Animation sampling, 15-second window |
These are the display thresholds. Third-party tags (DCM, Flashtalking, Sizmek, etc.) get higher limits because the vendor wrapper adds weight before the creative runs — we use 500 KB / 1 MB / 10 s / 30 requests / 15 scripts for tags inside CreativeValidator.
The checks that actually reject bundles
1. clickTag wiring
Every HTML5 bundle needs a top-level clickTag variable readable by the ad
server. GAM, DCM, Flashtalking, and Sizmek all scrape it at upload. Common mistakes:
-
Typo or casing:
clicktag,ClickTag,click_tag— none register. It must be exactlyclickTag. -
Nested scope: declared inside an IIFE or module scope, not on
window. -
Multiple clickTags: if the ad has more than one clickable area, declare
clickTag1,clickTag2, etc., and wire each anchor to the right one. -
Hardcoded URL: the value shipped in the zip is overwritten by the ad
server at runtime. If the anchor uses a string literal instead of
window.clickTag, no click-through tracking happens.
<script>
var clickTag = "https://example.com"; // placeholder, overwritten at runtime
</script>
<a href="javascript:window.open(window.clickTag)">...</a>
2. Initial weight must include fonts
The most common 150 KB overage: a 90 KB HTML + 40 KB JS bundle that pulls in a 120 KB custom WOFF2 font from Google Fonts. The font counts. Either subset it down to the actual glyphs used or switch to a system font.
3. document.write is disallowed
Publishers running inside SafeFrame or using post-load injection will break if the
creative calls document.write after DOMContentLoaded. Many
legacy ad-server wrappers still do it — including some DCM iframe/legacy tags. This is a
hard reject for Google Ad Manager.
4. Mixed content
Every resource the bundle pulls in must be served over HTTPS. Modern browsers block mixed content entirely inside SafeFrame, but you'll often find:
- Tracking pixels hardcoded to
http:// - Legacy CDN image paths
- Third-party JavaScript served from a non-SSL host (common with smaller measurement vendors)
5. Console errors
A creative that throws a JS exception during its first animation frame may still render the first keyframe, but everything after silently dies. Ad servers don't check this — but publishers do, and campaigns get pulled mid-flight.
6. Network-request leaks
Watch for creatives that fire a request every animation tick (lazy
img.src reassignment, or a debug-mode analytics hook). A bundle at 12
requests per second will pass the initial threshold and fail the 30-second sustained
check.
CreativeValidator runs all 16 IAB checks automatically — weights, requests, scripts,
load time, clickTag, SSL, console errors — against your .zip bundle.
The non-obvious gotchas
7. ClickTag must be escaped
If the creative uses window.open(clickTag) and the ad server passes the URL
unescaped, special characters in the landing page (e.g., query strings with
&) can truncate the URL. Always wire through
encodeURIComponent or use the ad server's escape macro.
8. Hidden iframes count toward request limits
A common trick for loading tracking pixels — injecting a 1x1 iframe pointing at a third-party domain — still counts as a network request. Teams often miss this because DevTools groups iframe requests under the iframe's document, not the parent.
9. Fonts and @font-face
If you use an @font-face rule, every variant declared fires a preload
regardless of whether it's used. If you only use regular-400, declare only regular-400.
10. Local storage and cookies
Most publishers strip third-party cookies already, and many SafeFrame implementations
block localStorage. Relying on either for state persistence within the
creative is fragile — use in-memory state instead.
11. GSAP and animation libraries
The full GSAP bundle is around 60 KB gzipped. That's 40% of your initial weight budget. Use the TweenLite/TweenMax subset that matches what you actually call, or switch to CSS animations for simple cases.
12. Fallback image requirements
Every HTML5 creative needs a static fallback image at the same dimensions. Publishers serve the fallback on browsers without JS, or as a backup if the HTML5 fails to render. Missing fallbacks don't block upload, but they do block serving in certain placements.
13. meta ad.size declaration
The HTML should include
<meta name="ad.size" content="width=300,height=250"> inside
<head>. Some adservers enforce this; others use it to auto-detect size.
Missing it makes you pick the size manually at upload, which is fine, but easy to
mismatch.
14. CPU-expensive transforms
Animating left/top/margin forces layout
recalculation on every frame. Animating transform/opacity is
compositor-only. A 5-second animation of left: 0 → 500px will spike CPU; the
same motion via transform: translateX(500px) won't.
15. Absolute URLs for all assets
If you use relative paths (./image.png), they resolve against the iframe's
base URL — which for ad-served HTML5 is the ad server's CDN, not your zip bundle. Always
use absolute paths or the adserver's asset macro.
16. Heavy ad guidelines (Chrome)
Since 2020 Chrome has unloaded iframes that use >4 MB total network, >60 seconds CPU, or >15 seconds wall time. These are less strict than IAB's, but once an ad is unloaded, no further tracking or click-through fires. A bundle that was "within tolerance" during QA can still get silently killed in the wild.
The QA pass that catches most of it
- Run the zip through an automated validator for all 16 checks.
- Manually open the fallback image and confirm dimensions match.
- Load the ad in a clean Chrome profile. Record the network tab for 30 seconds. Sum the bytes, count the requests.
- Open DevTools Console. Confirm zero errors during the first animation loop.
- Click through. Verify landing URL resolves, SSL is valid, no redirect loops.
- Resize the browser. Confirm the creative either scales cleanly or is locked to its declared size.
All of the above — plus 40 more checks — in a single drag-and-drop scan.
Upload HTML5 zip →