Published Sep 25, 2026

ISR-style revalidation can ship stale content—here’s the bug class

By Kevin Champlin

Friday deployment: a perfect graph, a wrong story

Last quarter, a Fortune 500 apparel brand (headless WordPress) asked why their “Summer Drop” landing page was showing the old product grid while everything else looked healthy. No 5xx spikes. No error logs. CDN cache hit rate was ~98%. Core Web Vitals were green.

The embarrassing part: the page was stale, but it was also valid per our rules. That’s the bug class I’m going to talk about: stale but valid cache invalidation. In other words, we weren’t serving broken JSON—we were serving JSON that passed schema checks, status codes were 200, and clients believed it was current.

We fixed it, but it cost us. We recovered about 6.5 hours of engineering time by killing one problematic caching strategy and tightening revalidation semantics. The “graph looked perfect” experience is exactly why I’m opinionated here.

Conventional wisdom says “ISR is safe.” That’s only true if freshness is provable.

You’ll hear it: “Use ISR (incremental static regeneration), set a revalidate window, and you’ll be fine.” I agree with ISR as a pattern. I disagree with how teams implement it in headless WordPress.

The typical flow looks like this:

  • WP publishes a content change (post updated, featured product changed, taxonomy updated).
  • We call an endpoint to revalidate a route (or mark a cache key dirty).
  • The next request triggers regeneration or serves cached HTML while regeneration happens.

The failure mode isn’t “cache invalidation failed.” The failure mode is: revalidation is decoupled from what you actually display. Your cached HTML might depend on data that changed in WordPress through a path you didn’t treat as a “revalidate trigger.”

The stale-but-valid mechanism (how it happens)

In that apparel brand system, the headless stack was: WordPress (ACF blocks + WooCommerce integrations) -> API -> Laravel edge service for aggregation -> React front-end using an ISR-like behavior at the CDN/edge layer.

We cached two different things:

  • Route HTML (pre-rendered) with a revalidation timer.
  • Product grid data fetched via an API call that itself was cached for performance.

Here’s the key bug class: the HTML became eligible for regeneration, but the underlying grid payload was still being served from an older cache key. Our HTML regeneration logic only checked “is the page route revalidating?” not “is every upstream dependency freshness-correct?”

So we got “stale but valid” responses: the HTML was generated using a payload that passed our validations (shape correct, IDs present, no errors), but the payload was stale by 2–5 hours. Customers saw the wrong products. Analytics dashboards didn’t show exceptions because nothing threw.

Concrete numbers from the incident

We later replayed requests against the same route over a 12-hour window:

  • CDN cache hit rate stayed at ~98% (so operationally it looked great).
  • About 0.8% of sessions hit a “stale but valid” state (hard to detect without comparing product IDs against WP revision metadata).
  • Cart/checkout impact was muted initially—conversion dip was only ~1.1%, but brand trust took the hit and we had to manually refresh featured product selections.

The lack of 5xx and the “looks fine” signals are exactly why this bug class is dangerous. Your monitoring needs correctness checks, not just availability.

Why ISR/revalidation breaks specifically with headless WordPress

WordPress adds two complications that a lot of teams ignore:

  • Revision isn’t the same as output. A post can update, but the rendered blocks (shortcodes, ACF relationships, WooCommerce queries) depend on other objects. “post modified” doesn’t imply “page output changed.”
  • Invalidation triggers are incomplete. You revalidate on post update hooks, but you might not revalidate when: featured image changes, taxonomy terms move, menu items change, or a WooCommerce product attribute used in a block updates.

In a headless setup, the route output is a composition. ISR is about revalidating the composed result, but your triggers are usually about single objects.

My rule: cache keys must encode dependency freshness, or you will ship stale-but-valid

I don’t believe in “hope the revalidate timer is short enough.” That’s how you end up trading correctness for CPU cycles until the day it quietly fails.

Instead, we implement dependency freshness markers. The idea:

  • Every cached response from the Laravel aggregation layer includes a data version stamp.
  • The stamp is derived from the specific WordPress objects used to build that response (post IDs + relevant taxonomy term IDs + WooCommerce product IDs + last-modified timestamps or WP revision IDs).
  • When the route regenerates, it refuses to use an upstream cached payload with an out-of-date stamp.

Implementation detail: the “stamp” can be a cheap hash like sha1(concat(...object_ids_and_wp_modified_gmt)). Yes, you pay a little CPU to compute it, but that cost is predictable and far less painful than debugging a correctness gap.

Concrete architecture choice: revalidate HTML, but also guard API payload caches

Here’s what we changed in practice on the apparel brand stack:

  • When WP hooks fire (post updated, term edited, product updated), we enqueue a revalidation job per affected route.
  • When Laravel renders the route’s data payload, it attaches dependency stamps into response headers (internal use) and into the JSON body as a field (external clients never see it).
  • The edge/ISR regeneration step requires the upstream payload stamp to match the expected dependency stamp for the route. If it doesn’t match, we treat it as a cache miss—even if the CDN says “fresh enough.”

This sounds heavy, but it was surgically contained. It reduced stale-but-valid responses from ~0.8% to effectively 0.0% in our replay window (and yes, we built a replay script to compare WP object modified times against the served payload).

Another real failure mode: race conditions in “revalidate while serving”

There’s a second way teams create stale-but-valid behavior: concurrent regeneration.

Common pattern: request A triggers regeneration, request B arrives before regeneration finishes, and B gets stale content marked “valid” (because status is 200 and the cache entry exists). If regeneration writes to cache before the dependency stamp is updated (or if there’s a multi-cache update ordering bug), B can observe a partially updated state.

We saw this once on the AI Showcase app when we used a background job to update block-level metadata. Requests could hit:

  • new HTML shell
  • old dynamic fragments
  • consistent schema + no exceptions

Fix: single-writer cache updates per cache key using a distributed lock (Redis) with strict ordering: update dependency stamp first (or store a “generation token”), then write final payload. If the lock can’t be acquired, serve the last fully consistent version—not a mixed one.

ISR vs “stale-while-revalidate”: pick your poison explicitly

Some teams try to “fix” ISR correctness by switching to stale-while-revalidate at the CDN. I don’t recommend this unless you can prove correctness tolerances.

My opinionated take: SWrR without dependency correctness checks is just ISR with a different mask. You still serve stale data. The only difference is how long it stays in the cache and how quickly the backfill happens.

If the UI shows personalized content (user-specific recommendations) or commerce-facing inventory/offer data, you should treat “stale” as unacceptable unless dependency freshness is provable.

Practical checklist for headless WP cache invalidation

  • Don’t invalidate only on post_id. Track dependencies: featured image, taxonomy terms, ACF relationship fields, WooCommerce product query inputs.
  • Encode freshness into cache keys or payload stamps. Your CDN/edge TTL is not a correctness mechanism.
  • Make regeneration atomic from the client’s perspective. Avoid partial writes and mixed states; use locks or generation tokens.
  • Add correctness monitoring. Compare served product IDs (or revision stamps) to WP modified data. Availability metrics alone won’t catch stale-but-valid.
  • Watch cache key collisions. Edge layers + language/currency parameters can silently collapse distinct variants if the key builder isn’t strict.

Closing thought

ISR and revalidation are fine, but treating them like correctness guarantees is how you ship “stale but valid” content—no errors, no alerts, and still a broken customer experience. If you can’t prove freshness of every dependency that composes the page, your invalidation strategy isn’t done.

One sentence to quote Monday: Cache TTL is not correctness—if you don’t encode dependency freshness and atomic regeneration, ISR will happily serve stale-but-valid pages.

At Champlin Enterprises, we treat cache invalidation as a production correctness problem, not a latency tweak: we model dependencies explicitly, add replayable verification, and ship guardrails directly into the Laravel + edge integration. See how we handle production-grade system behavior in our projects.

Free Tool

See exactly what AI costs — across every provider.

MyTokenTracker is a free, multi-provider intelligence platform with live pricing across 100+ models. Compare Claude, GPT-4o, Gemini, and more side-by-side — built for developers evaluating models, teams tracking API spend, and founders building AI-native products who want to stay cost-aware before it becomes a line item worth explaining.