You added ads.txt, deployed, and confirmed in a browser that https://your-domain.com/ads.txt shows the expected google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0 line. Hours later AdSense still shows “Earnings at risk” with ads.txt status: Not found. You re-deploy, wait a day, and the warning is unchanged.
Fastest fix: run curl -ILk https://example.com/ads.txt (apex) and confirm the trace ends in a single HTTP/2 200 with content-type: text/plain and a body that starts with google.com, pub-.... If you see two or more 3xx lines, a redirect that leaves your domain and then bounces again, an HTML body, or a 404, that is the break. Collapse the redirect to at most one hop that ends at a real 200 plain-text file.
Here is the part most guides get wrong: Google’s crawler does follow redirects for ads.txt. Per Google’s “Ensure your ads.txt files can be crawled” docs (current as of June 2026), crawling starts at the root domain, the root must return or redirect to the file, and only a single redirect outside the original root domain is followed. A second redirect — even one back to the same domain — stops the crawl. The crawler also tries both HTTP and HTTPS. So the real failures are redirect chains, redirects that never land on a 200, HTML served instead of plain text, a robots.txt that blocks the path, or a stale cached 404 — not “redirects” in general.
Which status are you seeing?
AdSense reports one of three states. Match yours before touching config.
| Status in AdSense | What it means | Where to look |
|---|---|---|
Authorized | File found, parsed, and your pub-XXXX ID matches | Nothing to do |
Unauthorized | File found and readable, but your publisher ID is not in it | Fix the file contents (wrong/missing pub- line), not redirects |
Not found | Crawler could not fetch a valid file (404, redirect chain, HTML body, robots block, timeout) | This article |
If you are Unauthorized, redirects are not your problem — copy the exact line from AdSense and paste the correct pub- ID into the file. The rest of this guide is for Not found.
Common causes
Ordered by how often each one triggers a Not found.
1. A redirect chain (two or more hops)
A single redirect is fine. Two are not. A common pattern: http://example.com/ads.txt → https://example.com/ads.txt → https://www.example.com/ads.txt. That is two hops, and if the first hop already left the root or a second redirect fires, the crawl stops.
How to spot it: curl -ILk http://example.com/ads.txt and count the location: lines. More than one redirect before the 200 is the break.
2. apex/www split with the redirect going the wrong way
Google’s rule: www.example.com/ads.txt is only crawled if example.com/ads.txt redirects to it. If you serve from www but the apex returns 404 (instead of redirecting to www), the crawler never reaches the file.
How to spot it: curl -Ik https://example.com/ads.txt. If the apex returns 404 while https://www.example.com/ads.txt returns 200, add an apex→www redirect (one hop) so the crawl can chain into the file.
3. robots.txt disallows the ads.txt path
Per Google’s docs, the crawler ignores ads.txt if robots.txt disallows the URL path the file sits on. A broad Disallow: / or a rule that catches /ads.txt silently blocks verification even though the file returns 200 in a browser.
How to spot it: curl https://example.com/robots.txt and check for any Disallow: line that matches /ads.txt. Fix by adding an explicit allow:
User-agent: *
Allow: /ads.txt
4. SPA framework serving HTML for the path (soft 404)
Single-page apps configure a catch-all that returns index.html for any unknown path. If ads.txt is not in your build output, the path returns 200 but with HTML. The crawler reads the body, finds no valid record, and reports the file as not found / malformed.
How to spot it: curl https://example.com/ads.txt | head -3. If you see <!DOCTYPE html> instead of google.com, pub-..., the SPA is swallowing the path.
5. CDN / WAF redirecting “bot” user agents
A bot-protection rule (Cloudflare, AWS WAF, Akamai) sends crawlers to a captcha or interstitial. The AdSense crawler hits the rule, gets a 302 to /challenge, and the file reads as missing.
How to spot it: curl -Ik -A "Mediapartners-Google" https://example.com/ads.txt. A cf-mitigated header, x-firewall-action, a 403, or a 302 to a challenge page means the WAF is the cause.
6. Caching layer serving a stale 404 from before the file existed
Your CDN cached a 404 from before ads.txt was deployed and keeps returning it from the edge for hours.
How to spot it: curl -Ik https://example.com/ads.txt shows cf-cache-status: HIT plus a 404, while a direct origin fetch returns 200.
Before you start
- Confirm your canonical domain — apex (
example.com) or www (www.example.com). Register that exact host in AdSense. - Know which DNS provider, host, and CDN are in the chain.
- Have the publisher ID handy:
pub-XXXXXXXXXXXXXXXX(AdSense → Account → Account information).
Information to collect
curl -ILk https://example.com/ads.txt— the-Lfollows redirects so you see every hop;-kavoids cert noise on the http→https leg.- The same against the www variant.
curl -Ik -A "Mediapartners-Google" https://example.com/ads.txt— simulate the AdSense content crawler UA.curl https://example.com/robots.txt— confirm/ads.txtis not disallowed.- Hosting platform (Vercel, Netlify, Cloudflare Pages, S3, custom Nginx, etc.).
- Your current
ads.txtcontent (first 5 lines). - The exact warning text from AdSense → Sites → your domain.
Step-by-step fix
Ordered from cheapest to most invasive.
Step 1: Trace the full redirect chain
curl -ILk https://example.com/ads.txt
curl -ILk https://www.example.com/ads.txt
Count the location: lines. The pass condition is: at most one redirect, and the final response is HTTP/2 200 with a plain-text body. Two or more hops, or a final non-200, is the failure.
Step 2: Collapse the chain to one clean hop ending in 200
If you redirect apex→www, that single hop is allowed — just make sure the apex does not also go http→https→www (that is two hops). Terminate TLS and host normalization at the edge so a single 301 lands directly on the www file.
A clean, crawler-safe setup is: pick one canonical host, redirect the other to it with exactly one 301, and make sure the file returns 200 at the destination. On Nginx, serve the file directly on whichever host is canonical and let the non-canonical host do a single redirect to it:
# Canonical host serves the real file
server {
server_name www.example.com;
location = /ads.txt { alias /var/www/ads.txt; default_type text/plain; }
}
# Apex does exactly ONE redirect to the canonical host
server {
server_name example.com;
location / { return 301 https://www.example.com$request_uri; }
}
If you cannot guarantee a single hop (for example a CDN inserts its own http→https redirect before yours), the safest workaround is to serve a real 200 ads.txt on both hosts so no redirect is needed for that path. On Vercel, vercel.json:
{
"redirects": [
{ "source": "/(.*)", "destination": "https://www.example.com/$1", "permanent": true, "missing": [{ "type": "header", "key": "x-skip-redirect" }] }
],
"rewrites": [
{ "source": "/ads.txt", "destination": "/ads.txt" }
]
}
On Cloudflare, add a Configuration Rule (or Worker) that exempts the /ads.txt path from any redirect and serves the file directly.
Step 3: Disable path-normalization redirects on the ads.txt path
If your host 301s case or trailing-slash variants, exempt ads.txt. On Netlify, _redirects:
# Serve ads.txt directly (status 200, no redirect) BEFORE any catch-all
/ads.txt /ads.txt 200
/* /:splat/ 301
The 200 status keeps it a direct serve, not a redirect. Order matters — the ads.txt line must come first.
Step 4: Unblock the path in robots.txt and your WAF
Confirm robots.txt allows /ads.txt:
User-agent: *
Allow: /ads.txt
If a bot-protection rule is firing, allow the AdSense crawler UAs (current as of June 2026):
Mediapartners-Google— the AdSense content crawler.Google-Display-Ads-Bot— used to verify a site when you add it to AdSense.AdsBot-Google— Google Ads ad-quality crawler, also seen on ads.txt fetches.
On Cloudflare → Security → WAF → custom rules, add a Skip rule:
(http.request.uri.path eq "/ads.txt") or (http.user_agent contains "Mediapartners-Google")
Set the action to Skip and check “All managed rules,” “Bot Fight Mode,” and “Rate Limiting” so none of them re-intercept the path.
Step 5: Make ads.txt a static file your SPA does not intercept
For Next.js, Astro, and Gatsby, place ads.txt in public/:
public/
ads.txt
These frameworks serve files in public/ directly without invoking the app. Verify in production:
curl https://example.com/ads.txt
# expected: google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0
For pure SPAs (Vite, CRA) the file must also live in public/. For a server-rendered framework, make sure the router matches /ads.txt before any catch-all:
// Express example
app.get("/ads.txt", (req, res) => {
res.type("text/plain");
res.sendFile(path.join(__dirname, "ads.txt"));
});
Step 6: Purge the CDN cache for the path
Even after a fix, a stale cached 404 keeps the warning alive:
# Cloudflare
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/ads.txt","https://www.example.com/ads.txt"]}'
For Vercel / Netlify, redeploying triggers a purge automatically. For S3 + CloudFront, invalidate /ads.txt from the CloudFront console.
Step 7: Wait for the recrawl
After the file is reachable, AdSense recrawls and clears the warning. Google’s docs say changes “may take a few days,” and on low-traffic sites that make few ad requests it can take up to a month. Don’t keep redeploying — give it at least a few days. Check status at AdSense → Sites → your domain.
How to confirm it’s fixed
curl -ILk https://<your-canonical-domain>/ads.txtshows at most one3xxand ends in a single200 OK.content-typeon the final response istext/plain(nottext/html).- The body is plain text starting with
google.com, pub-..., not<!DOCTYPE html>. curl -Ik -A "Mediapartners-Google" https://<your-domain>/ads.txtreturns the same200plus plain text — confirming the crawler UA is not blocked or redirected differently.curl https://<your-domain>/robots.txthas noDisallow:matching/ads.txt.- AdSense → Sites → your domain flips to
Authorizedwithin a few days.
Long-term prevention
- Use a single canonical domain (apex OR www) and register that exact host in AdSense from day one.
- Keep redirects to one hop. If a CDN adds its own http→https redirect, terminate TLS before your own host redirect to avoid stacking hops.
- Keep ads.txt in version control — never edit it through the host’s web UI (those editors can add a BOM).
- After any platform / CDN / DNS migration, re-run the curl checks above as part of the cutover checklist.
- Add a synthetic monitor (UptimeRobot, Pingdom) that hits
/ads.txtdaily and alerts on non-200 or a non-text/plaincontent type. - If you use a CMP or edge middleware that rewrites headers, confirm it leaves
/ads.txtuntouched.
Common pitfalls
- A UTF-8 BOM at the start of the file (added by some CMS editors) — the BOM byte makes AdSense read the first line as malformed.
Content-Type: text/htmlon the response — AdSense expectstext/plain. HTML renders fine in a browser but signals “wrong file.”- Putting ads.txt in a subdirectory like
/static/ads.txtand rewriting/ads.txtto it — some hosts log that internal rewrite as a redirect, adding an extra hop. - Registering only one of apex / www in AdSense while your single redirect runs the other way — register the host that actually serves the 200, or the one the apex redirects to.
- Assuming “the fix didn’t work” when it is just a stale CDN cache — always purge after fixing.
FAQ
Q: A browser loads ads.txt fine — why does AdSense still say Not found?
Browsers happily follow multi-hop redirects and render HTML; the crawler does not. Run curl -ILk and check for two or more redirects, a final non-200, or an HTML body. Any of those passes in a browser but fails the crawl.
Q: Does Google really refuse all redirects for ads.txt?
No. Google follows a single redirect that leaves the root domain (and the apex→www redirect is how www-canonical sites are supposed to work). The break is a chain — a second redirect, even back to the same domain, stops the crawl. See Google’s crawlability docs.
Q: AdSense says Unauthorized, not Not found — same fix?
No. Unauthorized means the file was read but your pub-XXXX ID isn’t in it. Copy the exact line from AdSense → Sites and paste your publisher ID into the file. Redirects are irrelevant in that case.
Q: I see a single 200 from curl but AdSense still says Not found. What now?
Try curl -Ik -A "Mediapartners-Google" <url>. A different response (302, 403) means a WAF treats the crawler differently — whitelist the UA as in Step 4. Also check robots.txt for a Disallow on /ads.txt. See adsense ads not showing for the downstream symptom.
Q: Do I need an ads.txt on every subdomain?
Only on the host you registered in AdSense Sites. If you registered www.example.com, the file must be reachable at www.example.com/ads.txt. A subdomain like blog.example.com needs its own ads.txt only if it is also AdSense-registered.
Q: Can I use sellers.json instead?
No. sellers.json is published by SSPs / exchanges, not publishers. AdSense requires ads.txt for publisher verification. They can coexist, but ads.txt is the required one. See ads.txt not found for the simpler “file just isn’t there” variant.
Q: How long until AdSense rechecks after I fix it?
Google says a few days; on low-traffic sites it can take up to a month. The status field updates asynchronously. Don’t disable / re-enable ad units during the window — it adds noise without speeding the recheck.
Tags: #AdSense #ads.txt #verification #redirects #Troubleshooting