You switched from the AdSense auto-ads snippet (or <ins class="adsbygoogle">) to Google Publisher Tag (GPT) because Ad Manager gives you ad scheduling, header bidding, and line-item targeting. You wired up gpt.js, called defineSlot and display, deployed — and every slot stays empty. The DevTools console either shows googletag is not defined, googletag.defineSlot was called without a matching googletag.display, or nothing at all.
Fastest fix: open DevTools, go to the Network tab, filter for gpt, and reload. If gpt.js is red or shows (blocked:...), an ad blocker or your CSP killed it — that is the single most common cause. If it loaded green (status 200), paste googletag.pubads().getSlots() into the console: an empty array [] means your defineSlot never ran; a non-empty array with blank divs means a slot-id mismatch. Those three checks resolve the large majority of cases.
GPT failures cluster into five buckets: the script never loads, the slot DOM id does not match defineSlot, googletag.cmd.push is not used (race condition), double-initialization from SPA navigation, and the wrong Google product entirely (AdSense vs Ad Manager). This article walks each case with concrete console diagnostics and the minimal correct snippet, verified against Google’s GPT reference as of June 2026.
Identify which case you are in (10 seconds)
Open DevTools console and type each line:
| You see | Likely cause |
|---|---|
Uncaught ReferenceError: googletag is not defined | gpt.js never loaded (network failure or ad blocker) |
googletag.apiReady is undefined (not true) | gpt.js is loading but has not finished, or never loaded |
googletag.pubads().getSlots() returns [] | defineSlot never executed |
getSlots() returns slots but <div> is empty | DOM id mismatch between defineSlot and the slot div |
googletag.defineSlot was called without a matching googletag.display warning | defineSlot ran but display(divId) was never called for that id |
Two googletag ad iframes load and one stays blank | Double init (script included twice, or SPA route change re-ran setup) |
Common causes, ranked by hit rate
1. gpt.js blocked by adblocker / CSP / network
uBlock Origin, Brave Shields, AdGuard, and corporate networks block securepubads.g.doubleclick.net by default. Your Content Security Policy may also reject it if you did not allowlist Google’s ad domains.
How to spot it: DevTools → Network tab → filter gpt. If gpt.js is red / status (blocked:csp) / (blocked:other), the script never loaded. Console shows googletag is not defined on any subsequent access.
Fix:
- CSP: allowlist
https://securepubads.g.doubleclick.net(script-src) andhttps://*.googlesyndication.com(frame-src + img-src). The current official source ishttps://securepubads.g.doubleclick.net/tag/js/gpt.js— if you copied an old snippet pointing athttps://www.googletagservices.com/tag/js/gpt.js, that host is deprecated and may be blocked or redirected. Update it. - Test in a clean Chrome profile with all extensions disabled — confirms the issue is client-side blocking, not your code.
- Do not try to “fix” adblocker-induced blank slots. Render a graceful fallback (your own content or whitespace) when GPT does not load.
2. googletag.cmd.push pattern not used
The correct pattern wraps every GPT call in googletag.cmd.push(function() { ... }). The cmd array is a command queue that gets drained when gpt.js finishes loading. If you call googletag.pubads() directly before the script loads, it throws.
<!-- Wrong: race condition -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
googletag.defineSlot('/123/leaderboard', [728, 90], 'banner').addService(googletag.pubads());
googletag.enableServices();
</script>
<!-- Right: use cmd.push queue -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag.defineSlot('/123/leaderboard', [728, 90], 'banner').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
</script>
Why: googletag.cmd = [] is initialized inline so cmd.push works synchronously. Once gpt.js loads, it replaces cmd with a function that runs each pushed callback immediately. This is the standard async-loader pattern.
Do not gate your code on typeof googletag !== 'undefined'. The googletag object exists almost instantly (you create it inline), but its API methods are not callable until the library finishes loading. Google’s documented readiness flags are googletag.apiReady (the core API is callable) and googletag.pubadsReady (pubads() methods are callable). The whole point of cmd.push is that you never have to check these yourself — GPT drains the queue the moment it is ready. Checking typeof googletag to decide when to call pubads() is the single most common mistake Google calls out.
Also keep the call order correct: page-level settings and targeting first, then defineSlot, then enableServices(), then display(). Setting targeting after enableServices() silently does nothing.
3. Slot div id does not match defineSlot
defineSlot(adUnitPath, sizes, divId) and googletag.display(divId) must both reference the exact same id as the slot’s <div>. Any mismatch — even capitalization — leaves the div empty.
<!-- Body -->
<div id="div-gpt-ad-leaderboard"></div>
<!-- Script -->
<script>
googletag.cmd.push(function() {
googletag.defineSlot('/123/leaderboard', [728, 90], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
googletag.enableServices();
googletag.display('div-gpt-ad-leaderboard');
});
</script>
How to spot it: in console, googletag.pubads().getSlots()[0].getSlotElementId() returns the id GPT thinks it should target. Compare to the actual <div id="..."> in the DOM. Any difference and the slot will not fill.
4. Double initialization from SPA route change
In React / Vue / Astro client-side routing, your component remounts when the route changes. If defineSlot runs every mount, you accumulate duplicate slot definitions and GPT silently refuses to register the duplicates. Worse, enableServices() should only fire once per page lifetime.
How to spot it: googletag.pubads().getSlots().length keeps growing on every route change; only the first slot fills, the rest stay empty.
There are two correct patterns, and which one you need depends on whether the slot <div> survives the route change:
- Same div stays mounted across routes (e.g. a persistent sidebar slot): define the slot once, then call
googletag.pubads().refresh([slot])to request a fresh ad without redefining. - The slot
<div>is unmounted and remounted (the common case — the ad lives inside the page component): each div id may only be displayed once per page lifetime, so a remount with the same id will not fill. Callgoogletag.destroySlots([oldSlot])(orgoogletag.destroySlots()to clear all) on the way out, then redefine anddisplay()on the way in. This is Google’s documented approach for single-page apps.
// App entry — runs once
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
// Persistent slot: define once, refresh on route change
let persistentSlot;
function mountPersistentSlot() {
googletag.cmd.push(function() {
if (!persistentSlot) {
persistentSlot = googletag.defineSlot('/123/leaderboard', [728, 90], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
}
googletag.display('div-gpt-ad-leaderboard');
googletag.pubads().refresh([persistentSlot]);
});
}
// Per-page slot that unmounts: destroy then redefine on each route
function onRouteChange(slot) {
googletag.cmd.push(function() {
if (slot) googletag.destroySlots([slot]);
// re-run defineSlot + display for the new page's div here
});
}
5. Wrong product entirely — you wanted AdSense, not GPT
GPT belongs to Google Ad Manager (formerly DFP). If you only have an AdSense account, you cannot use GPT — your ad unit path will not resolve to any inventory. The ad unit path format is /[network_code]/[ad_unit], e.g. /123456789/leaderboard; the network code is the numeric id of your Ad Manager network (find it under Ad Manager → Admin → Global settings → Network code, or read it off any working GPT tag). AdSense has no network code at all — it uses <ins class="adsbygoogle"> and a data-ad-client="ca-pub-XXXXXXXXXX" id, a completely separate system.
How to spot it: console request to securepubads... returns 200 but ad servers return 204 No Content. You only have AdSense in your Google account, not Ad Manager.
Fix: either link AdSense as a demand source inside Ad Manager (sign up for Ad Manager, link the account), or stay on AdSense <ins> tags. Do not mix AdSense Auto Ads with GPT on the same page — they fight over inventory.
Minimal correct GPT snippet (copy-ready)
<head>
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>
<script>
window.googletag = window.googletag || { cmd: [] };
googletag.cmd.push(function() {
googletag
.defineSlot('/NETWORK_CODE/leaderboard', [[728, 90], [970, 90]], 'div-gpt-ad-leaderboard')
.addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.pubads().collapseEmptyDivs();
googletag.enableServices();
});
</script>
</head>
<body>
<div id="div-gpt-ad-leaderboard" style="min-width: 728px; min-height: 90px;">
<script>
googletag.cmd.push(function() { googletag.display('div-gpt-ad-leaderboard'); });
</script>
</div>
</body>
Replace NETWORK_CODE with your Ad Manager network code. collapseEmptyDivs() hides the slot when no ad fills, preventing a giant empty box.
Shortest fix path
In hit-rate order:
- DevTools Network → confirm
gpt.jsactually loaded → if blocked, root cause is adblocker / CSP / network - Console:
googletag.pubads().getSlots()→ empty means defineSlot never ran; non-empty means proceed to step 3 - Match slot id between
<div id>,defineSlot(...), anddisplay(...)→ fixes most “script loaded but empty” cases - Wrap every call in
googletag.cmd.push(function(){ ... })→ fixes the race condition - Refresh slots on SPA route change instead of redefining → fixes the “ads work on first page but not subsequent” pattern
How to confirm it’s fixed
Run these four checks in the DevTools console on the live page; all four should pass:
googletag.apiReadyreturnstrue— the library loaded and initialized.googletag.pubads().getSlots().lengthequals the number of slots you defined (no more, no less — extras mean double-init).- For each slot,
googletag.pubads().getSlots()[0].getSlotElementId()matches a real<div id="...">in the DOM. - In the Network tab, the request to
securepubads.g.doubleclick.net/gampad/adsreturns200and a non-empty response. A200with an empty body (or agampad/adsrequest that 204s) means the code is correct but there is no matching demand — that is an inventory problem, not a tag problem (see the FAQ below).
If you have access to Ad Manager, the official Google Publisher Console (type googfc in the URL bar query string, or use the Ad Manager browser extension) shows per-slot fill status and the exact ad unit path GPT requested.
Prevention
- One ad system per site: AdSense
<ins>tags OR Google Ad Manager GPT. Never both. - Define slots once at the app root; use
refresh()on route changes. - Add a small dev-mode console banner showing slot count and fill rate so blank slots are obvious during QA.
- Keep
min-width/min-heighton every slot div equal to the smallest size you defined. Prevents CLS and makes empty slots visually identifiable. - Test in an incognito window with all extensions disabled before declaring a slot broken.
FAQ
Q: Can I use GPT with AdSense ad units? A: Yes — link AdSense as a backfill demand source inside Ad Manager. The actual page tag is GPT; AdSense fills inventory when no Ad Manager line item matches.
Q: Why use GPT instead of AdSense? A: GPT gives you direct-sold line items, ad scheduling, header bidding, audience targeting, frequency caps. AdSense is set-and-forget; GPT is for publishers who actively manage demand.
Q: My ads work locally but break on production. A: Three likely causes: (a) production CSP blocks Google ad domains, (b) you forgot to update the network code from staging to production, (c) your production domain is not approved in the Ad Manager site list. Check console for the specific error.
Q: googletag.pubads().getSlots() returns slots but they are still blank.
A: Inventory issue, not code issue. Either no line item targets that ad unit path, or your account is too new and has no demand yet. Check Ad Manager → Delivery → Line Items for the matching slot.
Q: First slot fills, second slot stays empty on the same page.
A: Almost always enableSingleRequest() is missing, so the second slot’s request is racing the first. Add googletag.pubads().enableSingleRequest() before enableServices(). Or you defined the second slot but never called googletag.display(secondDivId).
Related articles
- AdSense script installed blank
- AdSense ads not showing
- Ad blocker affecting RPM
- Manual ad units not rendering in static sites
External references:
- GPT: avoiding common implementation mistakes (Google for Developers)
- Get started with Google Publisher Tag (Google for Developers)