Some readers still see the old version of a corrected story
In short
A corrected story that reads correctly for you and wrongly for some readers is being held by exactly one cache layer, and you can identify it in about 10 minutes: request the URL with a cache-busting query string, request it again from a second network, and compare both against the record the API returns. The layer whose answer differs from the API is the one that never received the purge.
Key takeaways
- Find the layer before touching anything: a cache-busted request plus a second network isolates it in minutes.
- A purge API returning success means the request was accepted, not that every point of presence dropped its copy.
- The most common cause is an event-class mismatch — the webhook listens for publish and a correction emits update.
- A story that changed section or tags needs the pre-edit key set purged too, or the old lists keep the old copy.
- App readers are a separate layer with their own store, and no CDN purge reaches a service worker.
- The permanent fix is a verified purge: emit the full key set, retry on failure, then re-fetch and prove it.
One layer is holding the old copy, and 10 minutes of evidence will tell you which. Fetch the article's record from the content API and note its last-modified instant. Fetch the page URL with a unique query string appended. Fetch the plain URL, and read the Age and cache-status headers on the response. Then have somebody on a different network do the same. The layer whose answer disagrees with the API record is the one that never received the purge, and everything after that is a specific fix rather than a guess.
Do not flush the cache first. A full flush usually makes the symptom disappear, destroys the evidence that would have identified the layer, and guarantees the same incident recurs — at a worse moment, because the stories this happens to are the ones people are reading.
Prove which layer is holding it
- Read the source of truth. Fetch the story from the content API or preview endpoint and record its version or last-modified instant. If the API is wrong, this is not a caching problem at all and the investigation ends here.
- Bypass every cache with a unique query string. Request the URL with an unused parameter appended. Most edges treat that as a distinct object, so a correct response proves the origin and the build are fine and something downstream is holding a copy.
- Request the plain URL and read the headers. Age tells you how long the edge has held this object. The cache-status header — each CDN emits its own, and some now also emit the standardised one — tells you hit or miss. An Age far larger than your intended time-to-live is the whole answer.
- Repeat from a second network. A mobile connection, a colleague in another city, or an external checker. Different points of presence hold independent copies, and a purge that reached one region and not another looks exactly like a random subset of readers being wrong.
- Go around the edge. Request the origin directly with the host header set. A stale response here means a build artefact or an origin-side cache, not the CDN.
- Check the app and the browser separately. Reload the page with the cache disabled in developer tools. If the desktop browser is the only stale surface, it is a local cache or a service worker, and no CDN purge in the world will clear it.
The five layers, and what each looks like from the desk
| Layer | Who sees the old version | Evidence that confirms it | Who clears it |
|---|---|---|---|
| Edge cache at the CDN | Everyone in one region, or everyone until the time-to-live expires | High Age on the plain URL, correct content on the cache-busted URL | The purge path, once it names the right keys |
| Static build or incremental regeneration | Everyone, everywhere, including the cache-busted request | The origin serves the old copy while the API serves the new one | A rebuild, or the revalidation hook that should have fired |
| An intermediate proxy | One office, one ISP, one corporate network | Stale from that network only, fresh from a mobile connection | Nobody you employ — which is why time-to-live values matter |
| Browser cache | One person, usually the one who complained | Fresh on a hard reload or in a private window | The reader, or your Cache-Control headers next time |
| App shell or service worker | App readers only, sometimes for days | The web is correct and the app is not, on the same device | A client release or a cache version bump |
The third and fifth rows are the ones that produce the most confusing reports, because both make the problem look like it affects a random subset of readers. They do not: they affect a specific population you cannot see from the newsroom — one network, or one app version.
Ranked causes, most common first
- The purge never fired, because the event class did not match. This is the most common cause by a wide margin. The webhook subscribes to a publish event; a correction to a live story emits an update event, and nothing is listening. Check the event log for the story's save instant before anything else.
- The purge fired against the wrong key set. The story moved section, gained or lost a tag, or was removed from a curation slot, and only the post-edit keys were purged — so the old section index and the old tag page still hold it. Purging both the pre-edit and post-edit key sets is the fix, and what belongs in each is covered in surrogate keys and purging one story rather than the site.
- The purge fired and something dropped it. A queue with no retry, a rate limit, an expired credential, or a call that timed out and was never retried. This is invisible unless purges are acknowledged and failures alert.
- A static build was never regenerated. The page is rendered at deploy time or revalidated on a schedule, and the schedule is not the newsroom's. The symptom is distinctive: even the cache-busted request is stale.
- An intermediate cache with its own time-to-live. A reverse proxy in front of the origin, a partner's cache, an aggregator, or a corporate network. Nothing you purge reaches it, so the only control is the freshness headers you sent when the object was first served.
- A service worker serving cache-first. The app or progressive web shell has its own store and its own expiry policy, entirely outside the CDN. It clears on a version bump, which means a release.
Two adjacent symptoms are worth ruling out early because they look identical from the desk and have different causes. If the story went live at the wrong time rather than showing the wrong content, the problem is the scheduler — see scheduled stories that go live late or twice. If the page is fresh but its structured markup still describes the old version, that is a rendering path that did not re-run, and the labelling consequences are set out in markup that separates reporting, opinion and sponsored content.
Fix in this order, while the story is live
- Reach the reader first. Purge the article's own key, verify by re-fetching from 2 networks, and only then work outward to the section index, the front page and the feeds.
- Purge by key, not by URL, and never by flushing everything. A full flush sends every reader to the origin cold, and on a big story that turns a correction into an availability incident.
- Record what you purged and when. The instant a correction became visible to readers is a different fact from the instant an editor pressed save, and it is the one that matters if a complaint follows — which is why it belongs in a fact-check record that survives a complaint.
- Leave the diagnosis in writing before the adrenaline fades. Which layer, which evidence, which fix. Most publishers discover this class of bug 3 or 4 times before anybody writes it down.
A correction is not published when an editor saves it. It is published when the last cache in the path stops serving the old sentence.
The permanent fix: a publish event that is verified, not assumed
- Emit one event for every content change, of any class — publish, update, unpublish, retitle, re-tag — and let subscribers decide what they care about. Event-class mismatches disappear when there is one class.
- Carry the full key set in the event, both pre-edit and post-edit, computed by whatever knows the story's relationships rather than assembled by hand at the purge site.
- Acknowledge and retry. A purge is a distributed operation that fails sometimes; treat it like any other unreliable call, with a dead-letter path that raises an alert rather than a log line nobody reads.
- Verify by re-fetching. After the purge, fetch the article from 2 points of presence and compare a version marker in the response against the record. A verification that runs on every publish is a few lines of code and it converts an invisible failure into an alert.
- Publish a version marker in the HTML — a meta tag or comment carrying the story's last-modified instant. It costs nothing and makes every future instance of this bug a one-minute diagnosis instead of a 40-minute one.
- Bump the app's cache version on release, and set a short maximum age for article shells, so the mobile surface has a bounded worst case rather than an open-ended one.
The verification step is the one worth arguing for, because it is what turns purging from a hope into a system. It is small, unglamorous, plumbing-shaped work — the sort of thing we build as internal tools and ops systems, where the value is entirely in the failure it makes visible. If the same investigation keeps ending at the build layer rather than the edge, the question underneath it is architectural, and it is the one asked in why search traffic fell after the replatform, where stale rendering and crawl behaviour turn out to share a cause.
The rest of this silo sits under CMS and publishing platform engineering, and the platform work we do for newsrooms is described in our media and publishing practice.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why does an article page show a stale cached version after an update for only some readers?
Because caches are distributed, and a purge that reached some of them reached the rest late or not at all. Different points of presence hold independent copies, and browsers, app shells and corporate proxies each hold their own on top of that. Readers hitting a copy that received the invalidation see the correction; readers hitting one that did not see the old text. Which population is affected identifies the layer.
Should I just purge everything when a correction is urgent?
Not as a first move, and ideally not at all. A full flush sends every reader to the origin with a cold cache, and on the story that made the correction urgent that is an availability risk on top of an accuracy one. Purge the article's key, verify from 2 networks, and widen only if the verification fails. Reserve the full flush for the case where the key set itself is known to be broken.
How do I tell a CDN problem from a static build problem?
Request the URL with a unique query string appended. Most edges treat it as a new object and fetch from the origin, so a fresh response means the origin is right and the edge is holding a stale copy. A stale response to the cache-busted request means the origin itself is serving old content, which points at a build that did not regenerate or a revalidation hook that did not fire.
Why is the app showing the old story when the website is correct?
Because the app has its own cache that no CDN purge can reach. A service worker or a native HTTP cache stores responses locally under its own expiry policy, so a reader who opened the story before the correction can keep seeing it until that entry expires or the app updates. The controls are a bounded maximum age on article responses and a cache version that changes with each release.
- caching
- corrections
- CDN
- incident response
The work behind this page
Builds from our portfolio that this page draws on.
Read next
- Surrogate keys: purging one story without flushing the front pageOne story lives on six surfaces. The keys attached to those responses at render time decide whether a correction reaches all six, or whether somebody asks for a full flush at the worst moment.definition
- News sitemaps: the two-day window, and what the archive stays out ofA news sitemap is a rolling window, not an index. At publisher scale the failure is inclusion — archives, republished timestamps and paginated lists leaking into a file meant for two days of news.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation