
Visibility lag is the gap between a change existing in WordPress and that change reaching an end user. The edit is in the database or on disk, but visitors still get the old bytes. It compounds because a WordPress production request goes through multiple independent caches: a CDN edge, an HTTP page cache, a persistent object cache, PHP’s opcode cache, and the browser. Each cache expires on its own schedule. Add a staging environment and a domain cutover, and “the client says it’s not live” can mean five different faults.
This is a diagnostic guide for agency teams and in-house engineers working on Pagely. The goal is to isolate the layer in under five minutes with four commands, fix the right cache instead of all of them, and escalate with evidence when the fault is genuinely ours. Clearing everything does work, occasionally. It also hides the cause and dumps a cold cache onto your database at the worst possible moment.
Contents:
- What counts as visibility lag, and what doesn’t
- The four-test discriminator: where is the stale copy?
- Branch 1: is it a caching issue?
- Branch 2: is it a plugin or theme conflict?
- Branch 3: is it a staging-versus-production mismatch?
- Branch 4: is it DNS, TLS, or CDN propagation?
- Symptom, cause, fix: a reference table
- How do you clear caches on Pagely without causing a load spike?
- How do you test changes safely without touching production?
- When should you escalate to an engineer, and what should you send?
- Frequently asked questions
What counts as visibility lag, and what doesn’t
Visibility lag means the change is correct at its source and stale somewhere downstream. Distinguish between a problem caused by a change that was never saved, never deployed, or saved in the wrong environment. The solutions are completely different, so separate them first.
Definitions used throughout this piece:
- Page cache (HTTP cache): a full HTML response stored at the server or edge, keyed by URL and a few request attributes. Pagely’s stack includes an HTTP page cache layer (PressCACHE) in front of PHP.
- CDN: a geographically distributed cache of HTTP responses closer to the visitor. On Pagely that layer is PressCDN; many agencies add their own CDN or proxy in front of it.
- Object cache: a key-value store (commonly Redis) holding database query results, options, and transients between requests via the
wp-content/object-cache.phpdrop-in. - OPcache: PHP’s opcode cache. It stores compiled PHP so the engine doesn’t re-parse files on every request. If it doesn’t revalidate file timestamps, updated plugin or theme code can keep running the old version.
- TTFB: time to first byte. Useful here as a signal. A cached hit is usually an order of magnitude faster than an uncached PHP render on the same URL.
If the change isn’t present when you query the database directly, stop reading the cache sections. You have a deployment or environment problem, which is branch 3.
The four-test discriminator: where is the stale copy?
Run these four requests against the same URL, in this order. Replace example.com and the path with the real target, and ORIGIN_IP with the origin address your platform gives you.
A. Logged-in admin view – use your browser, cache disabled in DevTools
B. Anonymous request through whatever is in front of the site
curl -sS https://example.com/landing-page/ | grep -i "new headline"
C. Same request with a cache-busting query string
curl -sS "https://example.com/landing-page/?cb=$(date +%s)" | grep -i "new headline"
D. Straight to the origin, bypassing the CDN entirely
curl -sS --resolve example.com:443:ORIGIN_IP https://example.com/landing-page/ | grep -i "new headline"
Read the result pattern:
| A (logged in) | B (anonymous) | C (query string) | D (origin) | Most likely layer | Go to |
|---|---|---|---|---|---|
| Yes | No | Yes | Yes | HTTP page cache or CDN holding a stale HTML object | Branch 1 |
| Yes | No | No | Yes | CDN or external proxy caching the query string too | Branch 4 |
| Yes | No | No | No | Application layer: object cache, OPcache, or plugin output cache | Branch 1 and 2 |
| No | No | No | No | Change isn’t in this environment, or code didn’t deploy | Branch 3 |
| Yes | Yes | Yes | Yes | Visitor-side: browser cache, service worker, or corporate proxy | Branch 1, last part |
While you’re there, capture the response headers. They tell you which layer answered:
curl -sSI https://example.com/landing-page/ | grep -i -E 'age|cache|etag|last-modified|server|vary'
A non-zero value indicates that something upstream is serving a stored copy and will continue to do so until the Time to Live (TTL) expires. Also, check what the caching keys are based on; if they don’t vary with your test cookie, then your logged-in view may not accurately reflect what visitors see.
Branch 1: Is it a caching issue?
Work from the cheapest purge to the most disruptive. Resist the urge to flush everything at once; on a high-traffic site a cold object cache turns one stale page into a database load spike.
1. Confirm the change is actually in the database. Over SSH, in the site root:
wp post get 1234 --field=post_content | head -20
wp option get blogdescription
2. Purge the HTTP page cache for the affected URL. Purge the specific path first, not the whole site. In the Pagely dashboard you can purge cache for a site; path-level purges and the CLI equivalent are platform features to confirm with us before you script them (see reviewer notes).
3. Flush the object cache only if options, menus, or transients are stale. Check that a persistent object cache is even in play:
ls -l wp-content/object-cache.php
wp cache type
wp eval 'echo wp_using_ext_object_cache() ? "external" : "in-memory only";'
If there is no drop-in, the cache serves no purpose since it expires at the end of each request. If there is one:
wp transient delete --all # narrower: clears cached option-backed transients
wp cache flush # wider: drops the whole object cache keyspace
Tradeoff worth saying out loud: wp cache flush against a shared Redis instance can evict more than your site’s keys depending on how the cache is namespaced. Ask us how your instance is scoped before running it on a Monday morning. Transient deletion is typically the safer first step, addressing the most common issues such as stale menus, outdated WooCommerce product and price data, and stale “recent posts” blocks.
4. Rule out OPcache for code changes. If a plugin or theme file changed but behavior didn’t, PHP may still be executing cached opcodes. Important detail most guides miss: WP-CLI runs under the CLI SAPI with its own OPcache, so opcache_get_status() from wp eval tells you nothing about what PHP-FPM is serving your visitors. Don’t trust it. Confirm the file on disk changed, then ask us to confirm revalidation behavior for your plan.
md5sum wp-content/plugins/your-plugin/your-plugin.php
stat -c '%y %n' wp-content/plugins/your-plugin/your-plugin.php
5. Clear the visitor side last. If test E passes everywhere but the client still sees the old page, it’s their browser, a service worker, or their office proxy. In Chrome DevTools, check Application → Service Workers and enable “Bypass for network.” A stale service worker will happily serve last week’s HTML with every cache above it perfectly clean.
Branch 2: Is it a plugin or theme conflict?
Two patterns account for most of these: a second caching layer you forgot about, and an update that didn’t fully land.
Double caching. A performance plugin that writes full-page HTML or concatenated CSS/JS to disk will keep serving its own copy regardless of what the platform cache does. So will a plugin-managed CDN rewrite or an edge HTML cache enabled at your DNS provider. Inventory what’s active before you theorize:
wp plugin list --status=active --fields=name,version,update --format=table
wp theme list --fields=name,version,status,update
wp config list # look for WP_CACHE, custom cache dirs, environment constants
If you find a page-cache plugin alongside the platform cache, decide which one owns HTML and turn the other off. Running both means every purge has to happen twice, in the right order, forever.
Updates that didn’t land. “Plugin update not visible” usually means the version in the database advanced but the files didn’t, or the files did and something restored them. Verify against upstream checksums:
wp core verify-checksums
wp plugin verify-checksums --all
Failures here point at a partial update, a filesystem permission problem, or code deployed from git that overwrote the dashboard update on the next deploy. If your repository is the source of truth, updating plugins in wp-admin will be reverted. It is better to fix that workflow than re-run the update. Our guidance on update cadence and staging-first testing is in best practices for WordPress updates.
Isolate on staging, never production. Copy production to staging, then deactivate in groups rather than one at a time:
wp plugin deactivate optimizer-plugin cdn-helper-plugin
# retest, then
wp plugin activate optimizer-plugin
Flush transients between tests, or you’ll be reading cached output from the previous configuration and reaching confident, wrong conclusions.
Branch 3: Is it a staging-versus-production mismatch?
This branch is where “the change isn’t showing” most often turns out to mean “the change isn’t there.” Three sub-cases, in descending order of frequency.
The edit happened in the wrong environment. Someone logged into staging, edited, and reported against the production URL. Confirm which environment a shell or browser session is really in:
wp option get home
wp option get siteurl
wp config get WP_ENVIRONMENT_TYPE
Only half the change was promoted. Content lives in the database; templates, blocks, and plugin code live on disk. A file-only sync leaves new templates referencing posts that don’t exist in production. A database-only sync leaves new content rendering through old templates, which looks exactly like a cache problem. Diff the two environments before you promote:
# on each environment
wp plugin list --fields=name,version,status --format=csv > /tmp/plugins-prod.csv
# then locally
diff /tmp/plugins-staging.csv /tmp/plugins-prod.csv
Environment-specific configuration. Different table prefixes, a WP_HOME constant overriding the database, environment-gated feature flags, mu-plugins present in one environment only, or multisite domain mapping that routes your test request to a different site in the network. wp config list and wp site list (on multisite) answer these in seconds.
If you’re mid-migration or mid-domain-change, the overlap between “not promoted” and “not propagated” gets genuinely confusing. We wrote up the sequencing for that in advanced strategies for a smooth WordPress migration.
Branch 4: Is it DNS, TLS, or CDN propagation?
Propagation isn’t mystical. It’s bounded by the TTL you published before the change, plus the behavior of resolvers and proxies that don’t honor TTLs. If the old record had a 3600-second TTL, a resolver that cached it one second before your edit will serve the old answer for nearly an hour. Reduce the TTL to 300 seconds, a common pre-cutover value, at least one old TTL period before cutting over.
dig example.com A +noall +answer # current answer and remaining TTL
dig @1.1.1.1 example.com A +short # second opinion from another resolver
dig @8.8.8.8 example.com A +short
dig example.com CNAME +short # is traffic going through a proxy you forgot?
For certificates after a domain or hostname change, check what’s actually being served rather than what the dashboard says was issued:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
If the hostname isn’t in subjectAltName, issuance didn’t cover it. Two common blockers: a CAA record that doesn’t permit the issuing CA, and an HTTP-01 validation request that the CDN or a redirect rule intercepts before it reaches the origin. Check dig example.com CAA +short.
For the CDN layer itself: purge by path where you can, and remember that static assets are usually cached far longer than HTML. If a stylesheet changed but its URL didn’t, edges will keep the old file until TTL. The durable fix is versioning, not purging. Bump the ver parameter in wp_enqueue_style()/wp_enqueue_script(), or use filename hashing in your build. Domain cutover specifics are covered in our guide to migrating a WordPress site to a new domain.
Symptom, cause, fix: a reference table
| Symptom | Most likely cause | First action |
|---|---|---|
| Change visible logged in, old version anonymously | HTTP page cache holding stale HTML | Purge that URL’s page cache; verify age drops to 0 |
Change appears with ?cb=123 but not on the clean URL | Page cache or CDN object not invalidated | Purge by path; if it persists, purge the CDN layer too |
| Still stale with a unique query string | CDN or external proxy caching query strings, or edge HTML caching enabled upstream | Purge the CDN; check for a proxy CNAME and any edge page-cache feature |
| Menus, widgets, or site options stale everywhere | Object cache or transients | wp transient delete --all, then wp cache flush if needed |
| Plugin or theme updated, behavior unchanged | OPcache serving old opcodes, or files never updated | wp plugin verify-checksums --all; confirm file mtime; then ask us about OPcache revalidation |
| Plugin version reverts after each deploy | wp-admin update overwritten by git-managed wp-content | Update via the repository, not the dashboard |
| CSS change not applied, HTML is current | Long-lived static asset cached at edge and browser | Version the asset URL; purge the asset path |
| Correct on staging, absent on production | Partial promotion: files without database, or vice versa | Diff plugin/theme lists and home/siteurl; promote the missing half |
| Nothing you change appears anywhere | Editing the wrong environment or the wrong site in a multisite network | wp option get home, wp site list |
| Only the client sees the old page | Browser cache or a registered service worker | DevTools → Application → Service Workers → Bypass for network |
| New hostname shows a certificate warning | Certificate doesn’t cover the hostname, or validation blocked | Inspect subjectAltName; check CAA and redirect rules on /.well-known/ |
| Old site appears hours after a cutover | Resolver honoring the pre-change TTL | Wait out the old TTL; confirm with two public resolvers |
How do you clear caches on Pagely without causing a load spike?
Order matters more than breadth. Our recommended sequence:
- Confirm the source of truth.
wp post get,wp option get, or a file checksum. Don’t purge anything until the change is proven present. - Purge one URL at the HTTP cache. Narrowest possible blast radius. Retest with
curl -sSIand watchage. - Purge the CDN for the same path, plus any changed asset paths.
- Delete transients with
wp transient delete --allif options-backed data is stale. - Flush the object cache only if steps 2–4 didn’t resolve it. On a site doing meaningful traffic, schedule this outside peak hours; every request after the flush rebuilds its queries against the database.
- Full-site cache purge last. It’s the blunt instrument. It works, and it costs you a cold edge and a cold origin at the same moment.
There’s no single button that clears a visitor’s browser, a third-party proxy, and PHP’s opcode cache along with the platform caches, and anyone who tells you otherwise is selling something. Treat “clear all caches” as six distinct layers with six owners.
If you’re scripting this into a deploy pipeline, keep purges targeted to changed paths. A deploy that purges the entire cache on every push converts a small content update into a full origin warm-up, and on a seasonal traffic peak that’s the difference between a quiet release and a bridge call.
How do you test changes safely without touching production?
A workflow that holds up under client pressure:
- Reproduce on staging with production data. A staging environment with three posts and no cache layer will not reproduce a cache bug. Clone production, including the object cache configuration where possible.
- Test anonymously. Every diagnosis made while logged in as an administrator is a diagnosis of the bypass path, not the visitor path. Use
curl, a private window, or a second device on a different network. - Change one layer at a time. Deactivate one plugin group, purge the narrowest cache, retest. Two simultaneous changes produce a result you can’t attribute.
- Keep a known-good baseline. Record
wp plugin listandwp theme listoutput and your git SHA before any update, so “what changed” is a diff rather than a conversation. - Promote files and database together, with a documented order, and purge only the paths your change touched.
- Script the four discriminator tests. A 20-line shell script that runs tests B, C, and D against a list of URLs turns a 30-minute client call into a 30-second answer.
Where a different approach is better: if your team deploys several times a day and treats WordPress as a build artifact, invest in filename-hashed assets and path-scoped purges in CI rather than manual dashboard purges. If you publish a few times a week and run heavy page-builder templates, you’ll get more value from a longer page-cache TTL plus disciplined path purges than from aggressive invalidation. Both are legitimate; pick one and document it so the next person on the ticket doesn’t fight your defaults.
When should you escalate to an engineer, and what should you send?
Escalate when the discriminator tests point at a layer you don’t control: the origin serves the new content but the edge won’t let go of the old, purges succeed but age never resets, or curl to the origin returns stale HTML with an empty object cache. Those are platform-side and we should be looking at them, not you.
Send these five things and skip at least one round trip:
- The exact URL, plus whether the change is content, template, or plugin code
- The timestamp of the change and the timestamp of your last purge, with time zone
- Full output of
curl -sSIfor the affected URL - Results of tests B, C, and D from the discriminator table
- What you’ve already purged, in order
The reason we publish this instead of routing everything through a queue: your team is faster than any ticket when you have the right four commands, and the tickets that do reach us arrive with evidence attached. The platform is built on dedicated AWS infrastructure, offering shell and WP-CLI access, with engineers who can understand your stack rather than simply follow a tier-one script. If you’re evaluating whether that model fits how your agency runs client sites, compare plans or see what we build for enterprise WordPress hosting. For a client’s site that consists of three brochure pages and changes twice a year, a simpler hosting solution is the honest recommendation. The key differences we discussed regarding managed hosting for WordPress versus shared hosting are what truly matter here.
Frequently asked questions
How do I clear all caches at once on Pagely?
There’s no single action that clears every layer. Purge the HTTP page cache for the affected path, purge the CDN, then run wp transient delete --all and, if needed, wp cache flush over SSH. Browser caches, service workers, and PHP’s OPcache sit outside that sequence and need separate handling.
Why don’t my plugin or theme updates appear?
Either the files didn’t change or PHP is still running cached opcodes. Run wp plugin verify-checksums --all and check file modification times. If the version reverts after each deploy, your repository is overwriting dashboard updates, so update through the repository instead and redeploy.
How long does DNS or SSL certificate propagation take after a domain change?
DNS is bounded by the TTL published before your change, so a 3600-second TTL means up to an hour for resolvers that cached the old record. Lower TTLs to around 300 seconds before a cutover. Certificates are faster, but issuance can stall on a restrictive CAA record or an intercepted validation request.
How do I safely test changes without affecting production?
Clone production to staging, including cache configuration, and test anonymously with curl rather than as a logged-in administrator. Change one layer at a time, purge the narrowest cache between tests, and record wp plugin list output plus your git SHA so you can diff what changed.
Why do my changes show when I’m logged in but not to visitors?
Logged-in requests normally bypass the page cache, so you’re reading PHP output directly while visitors get a stored copy. Compare a curl request with a request carrying a unique query string. If the query-string version is current, an HTTP or CDN cache is holding stale HTML for that URL.
Should I disable the platform cache if my plugin already does caching?
Pick one owner for HTML. Running a page-cache plugin alongside the platform cache means every change needs two purges in the correct order, and a missed one looks identical to a platform fault. Keep the platform layer for HTML and limit plugins to asset optimization.

