Google Publisher Tag (GPT) Not Loading: Fix Empty Slots

Switched from AdSense `<ins>` to GPT and slots stay empty? Diagnose googletag is not defined, id mismatches, the cmd.push race, and SPA double-init in 10 seconds.

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 seeLikely cause
Uncaught ReferenceError: googletag is not definedgpt.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 emptyDOM id mismatch between defineSlot and the slot div
googletag.defineSlot was called without a matching googletag.display warningdefineSlot ran but display(divId) was never called for that id
Two googletag ad iframes load and one stays blankDouble 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) and https://*.googlesyndication.com (frame-src + img-src). The current official source is https://securepubads.g.doubleclick.net/tag/js/gpt.js — if you copied an old snippet pointing at https://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. Call googletag.destroySlots([oldSlot]) (or googletag.destroySlots() to clear all) on the way out, then redefine and display() 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:

  1. DevTools Network → confirm gpt.js actually loaded → if blocked, root cause is adblocker / CSP / network
  2. Console: googletag.pubads().getSlots() → empty means defineSlot never ran; non-empty means proceed to step 3
  3. Match slot id between <div id>, defineSlot(...), and display(...) → fixes most “script loaded but empty” cases
  4. Wrap every call in googletag.cmd.push(function(){ ... }) → fixes the race condition
  5. 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:

  1. googletag.apiReady returns true — the library loaded and initialized.
  2. googletag.pubads().getSlots().length equals the number of slots you defined (no more, no less — extras mean double-init).
  3. For each slot, googletag.pubads().getSlots()[0].getSlotElementId() matches a real <div id="..."> in the DOM.
  4. In the Network tab, the request to securepubads.g.doubleclick.net/gampad/ads returns 200 and a non-empty response. A 200 with an empty body (or a gampad/ads request 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-height on 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).

External references:

Tags: #AdSense #Monetization #Troubleshooting #GPT tag