Cache Races in a Publish Pipeline: Why Your IndexNow Ping Misses the Pages You Just Shipped
A deploy API returning 200 does not mean your new URLs are reachable at the edge. Here is how the deploy-then-ping race silently burns IndexNow submissions, and the verification sequence that stops it.
We publish from a scheduled agent: build, deploy, ping IndexNow, cross-post. For months the ping step returned HTTP 200 on every run and we filed that under done. Then we diffed the list of URLs we had submitted against what crawlers actually fetched, and found a batch of pages that had been pinged, fetched within the minute, and served either the pre-deploy version or a 404.
Nothing errored. Nothing retried. The pipeline reported success on every one of them.
The ping is fast; your edge is not
IndexNow inverts the crawl. Instead of waiting for a bot to rediscover your sitemap on its own schedule, you push a list of URLs and participating engines fetch them soon after. In our logs the first crawler hit typically lands within a minute or two of the ping. That speed is the entire value proposition, and it is also the bug.
The naive pipeline is three steps in one process:
- Build.
- Deploy — the API returns success.
- Ping IndexNow with the new URLs.
Step 2 returning success means the control plane accepted your artifact. It does not mean every edge node is serving the new HTML, and it does not mean cached responses for those paths were invalidated. So the timeline becomes: deploy returns at t+0, ping fires at t+0, crawler fetches at t+40s from a POP that has not caught up yet.
Negative caching is the sharp edge here. A path that did not exist yesterday can already have a cached miss at some edge — from a preview link you opened, a broken internal link, a scanner probing paths. Your deploy adds the page at origin. That edge keeps answering from its stored 404 until the TTL expires. The crawler asks exactly once, quickly, and it asks the edge.
Why the checks you would reach for first prove nothing
Three verification habits that feel rigorous and are not:
Curling the URL with a cache buster. Fetching https://yoursite/for-dev/slug/?v=123 returns your new HTML. That proves origin has the page. It also created a different cache key from the one you submitted. The canonical URL can still be serving stale while your check passes. We shipped two “verified” deploys this way before noticing the query string was doing the lying.
Checking the sitemap. Sitemaps usually carry a longer TTL than HTML pages. A crawler can read a stale sitemap alongside a fresh page, or the reverse. Sitemap freshness and page freshness are separate races; confirming one says nothing about the other.
Trusting the deploy tool’s completion message. Whatever the host — object storage behind a CDN, a worker, a container — the deploy call returns when the artifact is accepted, and propagation is asynchronous. On our worker deploys, the gap between “deploy returned” and “every edge we could sample served new content” ranged from a couple of seconds to over a minute. The tail is where pings die, and the tail is exactly what an unbounded async operation does not report.
The check that holds is narrower than any of those: fetch the canonical URL, no query string, with Cache-Control: no-cache on the request, and confirm you got a 200 and a marker unique to this build. We inject the build’s short git SHA into a meta tag on every page; the poll passes only when the fetched HTML contains the SHA the current run produced. A bare status check will happily pass on a stale-but-valid previous version of an updated article, which is the failure mode you are least likely to notice.
OpenCode
The polling, purging, and submission-log glue described here is about 80 lines of TypeScript with no interesting logic — good work to hand to a terminal coding agent while you specify the invariants.
Open source; bring your own model API key.
Affiliate link · We earn a commission at no cost to you.
The sequence that survives a cold edge
Ordering matters more than any individual check. What we run now:
- Build, emitting the git SHA into every page.
- Deploy.
- Purge explicitly — the exact new URLs, plus
sitemap.xml, plus any machine-readable indexes likellms.txtorarticles.json. Purging is what kills a cached negative response. Polling alone just waits out its TTL. - Poll each submitted URL individually until it returns 200 with the current SHA. Cap it: we allow roughly 90 seconds per URL at a 3-second interval. URLs that fail get dropped from the submission list rather than pinged anyway.
- Ping IndexNow with only the URLs that passed.
- Record what was submitted, with a timestamp and the SHA at submit time.
Step 6 is the one people skip and the one that converts belief into evidence. Without a submission log, a URL that was never pinged is indistinguishable from a URL that was pinged into a 404. We keep a small JSON file keyed by URL; a follow-up job checks days later whether those URLs show up in coverage reports and re-submits the ones that do not.
Two smaller things that cost us runs:
The key file is a single point of failure. The verification key file at your domain root gets fetched by the engine to confirm ownership. If it sits behind the same CDN and any build ships without it, a cached 404 there invalidates the entire batch — not one URL, all of them. Treat it as a static asset with a long TTL and never let it be conditionally generated.
Do not submit URLs that redirect. We moved older posts from a flat path to audience-prefixed paths. Submitting the old URL wastes the slot: the engine follows the 301, but the canonical you wanted indexed was the target all along. Submit whatever your URL helper produces for the current build, not the path the previous build used.
The same race applies well beyond search crawlers, and the blast radius elsewhere is worse. When a social post or a cross-post platform unfurls your link, it fetches your OG tags once and caches the result for a long time — sometimes indefinitely, with manual re-scrape as the only fix. A crawler that gets a stale page will come back. A link preview that gets a 404 keeps showing a broken card until you go clear it by hand. Gate the announcement fan-out on the same verification, not just the ping.
FAQ
Does a successful IndexNow response mean the page will be indexed?
Can I just sleep for 60 seconds after deploy instead of polling?
Why check for a build marker instead of just a 200 status?
Tools used in this review
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-08-12
Object Storage Lifecycle Policies: Cutting Storage and Egress Cost Without Losing Data You Need
How to audit an S3-compatible bucket, write lifecycle rules that actually pay for themselves, and avoid the transition fees, minimum-duration charges, and versioning traps that make bills go up instead of down.
2026-08-13
When There Is No API: Driving Chrome With the DevTools Protocol, and When Not To
CDP gives a scheduled agent a real browser when a site ships no API. Here is what it costs in memory and wall clock, the four failure modes that only surface on a cron, and the cases where you should not do it at all.
2026-08-12
Running a Small Kubernetes Cluster in 2026: k3s vs Talos vs a Managed Control Plane
k3s, Talos Linux, and a managed control plane solve different halves of the same problem. What each one actually costs you in money, upgrade work, and 2am debugging on a three-node cluster.
2026-08-12
Postgres Connection Pooling in 2026: PgBouncer vs Supavisor vs Driver-Side Pools
Postgres forks a process per connection, so pooling is not optional past a few dozen clients. How PgBouncer, Supavisor, and driver-side pools differ, and what transaction mode breaks.
2026-07-20
Blue-Green Deployments for Teams Without a Platform Engineer
Blue-green deployments do not require Kubernetes, a service mesh, or a dedicated platform team. Here is a working setup using nothing more than a reverse proxy, two ports, and a shell script.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.