D
P
0

WordPress & PHP

wp-admin Acting Haunted (Expired Nonce, AJAX Not JSON, Sidebar Vanishing)? That's CDN/Edge Cache, Not a Theme Bug

June 25, 2026·4 min read
wp-admin Acting Haunted (Expired Nonce, AJAX Not JSON, Sidebar Vanishing)? That's CDN/Edge Cache, Not a Theme Bug

A client reported that wp-admin felt haunted. The dashboard's theme and plugin lists showed stale data that no longer matched reality. Saving anything threw the classic WordPress nonce error:

The link you followed has expired.

Then a custom panel that called admin-ajax started failing outright. In the browser console, the JS was screaming because a response that should have been JSON arrived as HTML instead:

// Admin-side JS that expected JSON
fetch(ajaxurl, { method: 'POST', body: data })
  .then(function (res) { return res.json(); })
  .catch(function (err) {
    // Uncaught (in promise) SyntaxError:
    // Unexpected token '<', "<!DOCTYPE "... is not valid JSON
    console.error(err);
  });

And the creepiest part: the admin sidebar sometimes rendered, sometimes vanished. On the same page. Just from reloading. I spent a good while chasing a theme bug and a plugin bug. The application code was fine the whole time.

Why this happens

The cause was not in the theme, not in a plugin, and not in my PHP. It lived in a layer I never thought to suspect at first: the CDN / edge cache. Specifically, overly aggressive Cloudflare page rules were caching /wp-admin/ and /wp-admin/admin-ajax.php responses at the edge.

Once you see the mechanism, every "phantom" symptom snaps into focus:

  • Stale theme/plugin list. The admin page is served from the edge cache, so you are looking at an old snapshot rather than the current state of the install.
  • The "link expired" nonce. WordPress nonces are tied to the user and a time window. If a page containing a nonce gets cached and replayed to a later request, that nonce is stale (or belongs to a different user's context) by the time it reaches the server. Result: link expired.
  • admin-ajax returning HTML, not JSON. The edge serves a cached HTML response for an endpoint that is supposed to produce dynamic JSON. JS calling res.json() blows up instantly because what arrives is <!DOCTYPE html>.
  • The sidebar disappearing and reappearing. This is the biggest tell. You are hitting a mix of cache hits and misses. Some HTML fragments come from cache, some are fresh, and the two are inconsistent with each other from one reload to the next.

The signature: the weirdness changes on every reload and differs per URL. A real code bug is deterministic — the same input produces the same failure. This was not. And one experiment closed the case: hard refresh or bypass the cache, and it all goes away. If skipping the cache cures the problem, the problem is in the cache, not in your application.

There is a quieter danger here too. Caching a logged-in response and serving it to a different request is not just a display glitch — it can leak one user's content to another. For an admin area, that is a security issue, not merely an annoyance.

The fix

The rule is simple: /wp-admin/ and /wp-admin/admin-ajax.php must never be cached at the edge. Neither should requests from a logged-in user. WordPress already marks a logged-in session with a cookie, so the presence of that cookie is exactly the signal I want to use to bypass the cache entirely.

On Cloudflare, the fix means creating rules that bypass cache for the admin paths and ensuring the WordPress login cookie breaks the cache. Conceptually the rules look like this:

# Admin paths: never cache at the edge
URL match: *old-site.com/wp-admin/*
  -> Cache Level: Bypass
 
URL match: *old-site.com/wp-admin/admin-ajax.php*
  -> Cache Level: Bypass
 
# Also bypass any request carrying the WordPress login cookie
Cookie contains: wordpress_logged_in_
  -> Cache Level: Bypass

If you prefer to do this at the web server level, the principle is identical: never let anything under /wp-admin/ land in a shared cache, and never cache a request carrying the wordpress_logged_in_ cookie. The vendor and the syntax do not matter — what matters is that admin paths and logged-in requests always punch straight through to the origin.

Once those rules were live, I did a single hard refresh to purge the stale versions from the edge. The nonces became valid again, admin-ajax returned proper JSON, the plugin list was accurate, and the sidebar stopped flickering out of existence.

The takeaway

When wp-admin behaves inconsistently — different per page, changing between reloads, healed by a hard refresh — do not start tearing apart the theme. That pattern is the signature of an edge-cache artifact, not an application bug. Application code fails deterministically; caches fail at random.

So my first question now when admin acts strange is no longer "which plugin is misbehaving?" but "what is the hosting and CDN stack, and is anything caching /wp-admin/?" Diagnose the infrastructure before the theme. More often than you would think, the answer sits one layer above the code you are staring at.