AdSense status says “Approved.” You pasted the snippet. Open the article — the spot where the ad should be is just empty space. People immediately suspect the code, but the code is right about 80% of the time. The real culprits are: AdSense is still warming up your slots, the slot can’t get a fillable impression, or your page is missing a signal Google needs to serve an ad.
This article goes through them in the order they actually break.
Common causes
Ordered by hit rate, highest first.
1. First 24-48 hours post-approval — slots are warming up
After approval, AdSense needs roughly 1-2 days to start serving on new slots. The dashboard says “Approved” but the auction hasn’t started filling your inventory yet.
How to spot it: Open the DOM at the ad slot. You’ll see <ins class="adsbygoogle" data-ad-status="unfilled">. If your approval was < 48 hours ago, it’s the warm-up. Wait.
2. ads.txt missing or wrong
If https://yourdomain.com/ads.txt doesn’t exist, or has the wrong publisher ID, AdSense won’t serve. Search Console and AdSense both warn about this, but the warnings get missed.
How to spot it:
curl -s "https://yourdomain.com/ads.txt"
You should see:
google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
The pub-ID must match your AdSense account exactly. Missing file, missing/extra fields, or wrong ID = no ads.
3. The ad slot doesn’t have a fillable width
AdSense responsive ads need the parent container to have a defined, non-zero width before the script runs. If your ad is inside a display:none block, a 0-width sidebar, or a hidden mobile drawer, the unit returns unfilled.
How to spot it: In DevTools → inspect the <ins class="adsbygoogle">. Check its computed width. If 0px, find the parent container and give it a real width.
4. Ad slot ID typo
A common copy-paste mistake: data-ad-slot="1234567890" doesn’t match any slot you actually created in AdSense → Ads → By ad unit. AdSense silently returns no ad rather than erroring.
How to spot it: In AdSense → Ads → By ad unit, list your ad units. Match each data-ad-slot value in your code against this list. Any mismatch = no ad.
5. Page topic has no advertisers right now
AdSense has roughly 5x the demand for “credit card” pages than “vintage typewriter restoration” pages. Pages on extremely niche topics may genuinely have no fillable bids at a given moment.
How to spot it: Test the same code on your homepage (broader topic). If the homepage fills but the niche article doesn’t, it’s inventory, not code.
6. AdSense detected and is muting “invalid traffic”
If you’ve been F5-ing your own page, clicking your own ads, or you have bot traffic, AdSense will quietly disable serving on your site to protect advertisers — no warning email until the situation persists.
How to spot it: AdSense → Policy center. Look for “Invalid traffic” notes. Even without a formal warning, suspicious traffic patterns reduce fill silently.
7. Page is blocked by a CMP / consent gate
If a Consent Management Platform sits in front of your page and the user hasn’t accepted ad consent, AdSense respects the gate and serves no personalized ad — and in some configurations, no ad at all.
How to spot it: Open the page in incognito, accept all cookies in the consent dialog. If ads now appear, the CMP is the cause. Decide your default for non-consenters: contextual (non-personalized) ads or nothing.
Shortest path to fix
Step 1: Read the ad slot’s data-ad-status attribute
Open the page, inspect the ad placeholder, and look at the <ins> tag:
data-ad-status value | Meaning | Fix |
|---|---|---|
unfilled | Slot loaded but no ad served | Check inventory (#5), CMP (#7), or wait |
| missing entirely | Script never executed on this slot | Code or display:none issue |
filled | Ad served (but maybe blocked downstream) | Ad blocker, CSP, iframe sandbox |
This one attribute eliminates half the diagnostic tree.
Step 2: Verify ads.txt
curl -s "https://yourdomain.com/ads.txt"
diff <(curl -s "https://yourdomain.com/ads.txt") <(echo "google.com, pub-YOURID, DIRECT, f08c47fec0942fa0")
If empty or different, create / fix the file at the root of your site. Astro: put in public/ads.txt. Next.js: public/ads.txt. Vercel: same. Wait up to 24 hours for AdSense to re-crawl.
Step 3: Verify the ad unit exists and the slot ID matches
In AdSense → Ads → By ad unit, copy the slot ID. Grep your codebase:
grep -rn "data-ad-slot" src/
Every value should match a unit in AdSense. Delete unused snippets.
Step 4: Force a fillable width
If the slot is inside a flex / grid container that collapses on certain breakpoints:
.ad-slot {
display: block;
min-width: 250px; /* matches AdSense minimum */
min-height: 100px;
}
Or wrap the <ins> in a div with explicit width: 100%; min-height: 250px.
Step 5: Test with personal data off
Open in incognito → reject the CMP → see if any ad shows. If nothing, your CMP is configured to block all ads for non-consenters. Switch to “non-personalized ads” as the default for non-consenters:
<script>
(adsbygoogle = window.adsbygoogle || []).requestNonPersonalizedAds = 1;
</script>
Step 6: Wait 48 hours after fixing
AdSense’s reconciliation isn’t instant. After you fix the code or ads.txt, give it 24-48 hours before declaring it broken again.
Prevention
- Add an
ads.txtvalidity check to your CI:curl -sf yourdomain.com/ads.txt | grep -q "$PUB_ID". - Lint all
data-ad-slotvalues against a known list at build time. - Keep ad slots out of conditionally-hidden containers; render the placeholder always, hide ads only via the publisher controls.
- Don’t reload your own page repeatedly — it can trigger invalid traffic flags.
- Make sure your CMP allows at least non-personalized ads for non-consenters.