You’ve added <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-..."> plus the <ins class="adsbygoogle"> tag. DevTools → Network confirms adsbygoogle.js returned 200. But after refreshes, after hours, the slot stays a blank <ins> block. This is the most “looks correct, isn’t” failure mode in AdSense, and it almost always traces to one of seven wiring or account issues, not a code bug.
Fastest path: read the data-ad-status attribute that AdSense writes back onto your <ins> tag (Step 1 below). If it says unfilled everywhere on a brand-new account, you are most likely still in review or inside Google’s 24-48h serving warm-up — that is not a bug, and no code change will fix it. If the attribute is missing entirely, your push({}) never ran. Those two readings cover the large majority of cases.
Common causes
Ordered by hit rate, highest first.
1. Account is under review, or inside the 24-48h serving warm-up
This is the biggest cause for new publishers, and the article most other guides skip. While your AdSense application is still under review, every slot shows blank — Google does not serve real ads until the account is fully approved. Even after approval, it can take up to 48 hours (as of June 2026) for ads to begin serving after you add or edit ad code or connect a new site, and Auto ads often appear on mobile a day or two before desktop.
How to spot it: check the AdSense dashboard home. A “Getting ready” / “We’re reviewing your site” banner, or a site marked “Requires review” / “Needs attention” under Sites, means no real ads will serve yet. Review typically takes a few days but can run 2-4 weeks. There is no code fix — wait, then re-check after 48h.
2. (adsbygoogle = window.adsbygoogle || []).push({}) never fires
The script loads, but the push({}) call that activates the slot is missing or sits somewhere that never runs (inside a route-change handler that didn’t fire, gated by an if, or stripped between SSR and hydration).
How to spot it: in the DevTools console run window.adsbygoogle.loaded. If it returns undefined or false, the push never ran. The <ins> will also have no data-ad-status attribute at all (not even unfilled).
3. Slot rendered with width = 0
The <ins> element exists inside a parent that is display: none, has 0 width at the current breakpoint, or sits in a tab/accordion that isn’t open. AdSense rejects 0-width responsive slots and the slot stays blank.
How to spot it: open the console. A 0-width slot throws a visible error: Uncaught TagError: adsbygoogle.push() error: No slot size for availableWidth=0. You can also inspect the <ins> → Computed → width; if it’s 0px, that’s it. Common in flex children whose width isn’t known until layout settles, position: absolute/float parents, and width: max-content boxes wrapping the ad.
4. Slot pushed twice (the duplicate-push error)
If two push({}) calls target the same <ins> — or React/Next StrictMode double-mounts your ad component in development — the second push throws and the slot can end up blank. The exact console error is: Uncaught TagError: adsbygoogle.push() error: All 'ins' elements in the DOM with class=adsbygoogle already have ads in them.
How to spot it: check the console for that string, and count your push calls. Each <ins> should be pushed exactly once. In React/Next this is the classic StrictMode double-render symptom.
5. Multiple <script async ...adsbygoogle.js> tags
If your layout loads the AdSense script in two places (once in the head, once in a component), it loads twice and the second copy can fail to bind. Slots end up half-pushed.
How to spot it: in the console run Array.from(document.querySelectorAll('script[src*="adsbygoogle"]')).length. It should return 1.
6. <ins> markup is invalid
Common typos: data-ad-clinet (sic), data-ad-slt, missing class="adsbygoogle", missing style="display:block", or React mangling kebab-case when you wrote dataAdClient.
How to spot it: View Source (the real HTML, not the React render tree). The shipped markup must contain exactly class="adsbygoogle" and data-ad-client="ca-pub-...".
7. Page below the content threshold, or your IP is rate-limited
Thin pages — a 50-word stub, a category page that’s just a link list, a search-results page — don’t have enough content to host an ad, so the auction returns nothing and data-ad-status="unfilled" fires. Separately, if you’ve been hammering F5 to test, AdSense may throttle serving on your own IP for the day.
How to spot it: if long articles fill but thin pages don’t, it’s the content threshold. If a known-good page is blank only for you, open it from your phone on cellular data (a different IP) — if ads serve there, you’re being rate-limited locally.
Shortest path to fix
Step 1: Read data-ad-status from the rendered <ins>
In the console:
document.querySelectorAll('ins.adsbygoogle').forEach((el, i) =>
console.log(i, el.getAttribute('data-ad-status'), el.offsetWidth));
As of June 2026 the attribute has three values: filled, unfilled, and unfill-optimized (no ad returned, slot now optimized by AdSense). Map your reading to a cause:
| Status | Width | Meaning |
|---|---|---|
null / missing | any | Push never ran (cause #2, #4, #5, or #6) |
unfilled / unfill-optimized | > 0, every slot, new account | Account under review or in 24-48h warm-up (cause #1) |
unfilled / unfill-optimized | > 0, only some pages | Content threshold or local rate-limit (cause #7) |
unfilled | 0 | Width problem (cause #3) |
filled | > 0 | Working — if you still see nothing, check ad blockers |
A reading of null/missing means the script never finished binding the slot; unfilled/unfill-optimized means it finished but the auction returned nothing.
Step 2: Rule out the account first (don’t debug code that’s fine)
If Step 1 shows unfilled/unfill-optimized on every slot of a new account or new site, stop debugging code. Open the AdSense dashboard → Sites and confirm the site shows Ready, not “Requires review” or “Getting ready.” If it’s still in review, or it was approved less than 48h ago, wait — that is expected behavior, not a wiring bug.
Step 3: Force the push call (test it manually)
In the console:
(window.adsbygoogle = window.adsbygoogle || []).push({});
If a slot fills now but didn’t before, your push code wasn’t running. For a plain page, place the push inline immediately after the <ins>:
<ins class="adsbygoogle" ...></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
For an SPA (React / Next / Astro islands), the AdSense script only detects full page loads, not client-side navigations — so re-run the push on every route change, key the <ins> on the route path so it remounts cleanly, and guard against the duplicate-push error:
useEffect(() => {
try {
(window.adsbygoogle = window.adsbygoogle || []).push({});
} catch (e) {
// StrictMode double-mount or "already have ads" — safe to ignore
}
}, [router.asPath]);
Step 4: Fix the markup
Use this exact template; do not rename attributes:
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
data-ad-slot="1234567890"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
In React, kebab-case data-* attributes are preserved as written, so use data-ad-client (not dataAdClient). Astro keeps attributes as-is. If your framework rewrites them, render the block via raw HTML instead.
Step 5: Remove duplicate script tags
grep -rn "adsbygoogle.js" src/ public/
It should appear in exactly one layout/template file. Delete the duplicates.
Step 6: Force min-width on slots
.adsbygoogle {
display: block;
min-width: 250px;
min-height: 100px;
}
Don’t place ads inside display: none until they’re visible — that’s what triggers availableWidth=0. Use visibility: hidden if you must hide one without collapsing layout.
Step 7: Hide slots that legitimately stay unfilled
A genuinely unfilled slot (cause #7) shouldn’t leave a visible gap. Google’s own recommended rule collapses those:
ins.adsbygoogle[data-ad-status="unfilled"] {
display: none !important;
}
Note this only fires after the script runs; an ad blocker that stops adsbygoogle.js entirely never sets the attribute, so the gap stays. AdSense already auto-collapses unfilled units that are out of viewport and only leaves a blank space for in-viewport units to avoid page reflow.
Step 8: Test from a fresh IP
Phone on cellular, a friend’s network, or a VPN. If ads serve there, you’re rate-limited locally — AdSense recovers within about 24 hours, so stop reloading your own site to test.
How to confirm it’s fixed
- Hard-refresh (
Cmd/Ctrl + Shift + R) on a real published URL, notlocalhost— AdSense will not serve onlocalhostor non-allowlisted hosts. - Re-run the Step 1 snippet. Every visible slot should report
filledwithoffsetWidth > 0. - The console shows no
TagError(No slot size for availableWidth=0oralready have ads in them). - In the AdSense dashboard, the site reads Ready and the page URL is allowed under Sites.
FAQ
My account was just approved but slots are still blank — is something broken? Probably not. After approval (or after adding/editing ad code, or adding a new site) it can take up to 48 hours for ads to start serving as of June 2026. Auto ads commonly appear on mobile first. Wait 48h before changing any code.
data-ad-status is unfilled (or unfill-optimized) but the page is long and approved. Why no ad?
unfilled means the push ran and the auction simply returned no ad — usually low advertiser demand for that page/audience, a niche topic, or temporary local rate-limiting from your own repeated reloads. It is not a markup bug. Add the Step 7 CSS so the gap collapses, and re-test from a different IP.
I get No slot size for availableWidth=0 in the console. What now?
The <ins> is rendering inside a 0-width container — a collapsed flex child, a display: none parent, or a position: absolute/float box with no width. Give the slot’s container a real width (often width: 100%) and the min-width/min-height from Step 6, and don’t push until the element is visible.
I get All 'ins' elements... already have ads in them. How do I stop it?
You’re pushing the same slot twice. In React/Next this is StrictMode double-mounting in development — wrap the push in a try/catch (Step 3), push once per <ins>, and key the ad component on the route so it doesn’t double-fire on navigation.
Why do ads work on my phone but not my laptop? Either an ad blocker / privacy extension in your desktop browser, or AdSense rate-limiting your home IP after repeated test reloads. Disable extensions and stop F5-testing your own site; serving recovers within roughly 24 hours.
Should I just use Auto ads instead of manual <ins> units?
Auto ads remove the per-slot wiring (no <ins> typos, no manual push), but they still don’t serve during review or the warm-up, and they need time to scan your pages. Many sites run a hybrid: a few manual units in proven spots plus Auto ads for the rest. Neither fixes a blank caused by cause #1.
Prevention
- Route every slot through one
<AdSlot>component / partial — no copy-paste markup, no typos. - Pin the AdSense script to exactly one place (root layout or document head).
- Lint that every
<ins class="adsbygoogle">has exactly one matchingpush({}). - Keep a global
.adsbygoogle { min-width: 250px; }rule plus the unfilled-collapse rule from Step 7. - On new sites, expect blank slots for up to 48h and confirm Ready in the dashboard before assuming a bug.
- Don’t reload the same page repeatedly when testing — use a phone or a second browser.
Related
- AdSense ads not showing
- No ads available on some pages
- Manual ad units not rendering in static sites
- Ads slow down the website
For the official reference, see Google’s data-ad-status documentation and what to do when your site isn’t ready to show ads.