Caching layers: three caches, three different promises.
Page, object and browser caches solve different problems. Learn what each one keeps, what makes it stale, and how we publish the site you are reading.
Cache decisions, in numbers
Configured HTML lifetime in one incident, not a year of observed downtime.
How long a poisoned redirect could remain cached without a purge.
Individual purge requests during a nightly sync, before they were batched.
A suggested starting TTL for public HTML, paired with deliberate invalidation.
Each one makes a different promise.
Three caches, each with a different promise. A page cache promises a whole response. An object cache promises a query result. A browser cache promises nothing to anyone but one visitor. Treat them as one thing called "the cache" and you will clear the wrong one at two in the morning.
| Cache layer | What it answers | What goes wrong | What to check |
|---|---|---|---|
| Edge page cacheA CDN keeping whole responses, keyed by what it was told matters. | Anonymous page views, assets, feeds and bots, answered before PHP runs. | A stale page that looks fine, served confidently to everyone until something purges it. | The key does not match what the origin varies on, or nothing purges what changed. |
| Object cacheRedis or Memcached in front of the database, holding query results, options and transients. | The repetitive reads that would otherwise make the database the bottleneck on every request. | Settings that will not save, jobs stuck at two percent, and one pod serving yesterday's layout. | Separate per-pod caches can disagree. Shared caches still need correct invalidation and isolation between environments. |
| Browser cacheEach visitor's own copy, governed by max-age and the asset name. | Repeat visits and every static file with a hashed name, for free. | A visitor who cannot see the fix for a day no matter what you purge. | A long max-age on HTML, or an asset name that did not change when its contents did. |
What the edge thinks "the same request" means.
A cache key is the list of request attributes the edge uses to decide that two requests are the same. A useful key preserves real response differences without splitting identical pages into needless copies.
A real request on this site
https://udx.io/wordpress-on-linux/caching?jsonOpen it to see the layout data behind this chapter. Remove ?json and the same path returns HTML. Those are different responses, so they need different cache entries.
On this site, udx.io and /wordpress-on-linux/caching identify the resource. Keep hostnames separate when the responses differ.
The ?json flag changes this page from HTML to layout data. It belongs in the key. Tracking parameters should not create another copy of identical content.
Authentication or cart cookies need deliberate bypass rules. Removing them from a key is not enough if personalized responses can still reach a shared cache.
Our deployed page key currently includes Accept, Origin and Host. Review each against actual response variation before removing it.
This React-based theme requests a page and its layout data separately. Invalidating the HTML alone can leave the rendered page stale.
If an unkeyed attribute is forwarded and changes the origin response, the cache can reuse the wrong answer. Review forwarding and key policies together.
Every change, against every object it must clear.
A purge map connects each content change to the cached responses it affects. This simplified before-and-after map comes from the operating record. A global purge covers every page even when there is no separate URL-level purge.
Scroll across the map on smaller screens.
| Content change | The permalink | Homepage | Archives and feeds | Sitemap | Every page |
|---|---|---|---|---|---|
| Post edited | ✓ | − | − | − | − |
| Menu changed | − | − | − | − | − |
| Theme option saved | − | − | − | − | − |
| Post trashed | ✓ | − | − | − | − |
| Deploy | − | − | − | − | − |
| Nightly sync | ✓ | − | − | − | − |
Before: the save hook purged the edited permalink and nothing else. HTML lived at the edge for a day.
| Content change | The permalink | Homepage | Archives and feeds | Sitemap | Every page |
|---|---|---|---|---|---|
| Post edited | ✓ | ✓ | ✓ | ✓ | − |
| Menu changed | − | ✓ | − | − | ✓ |
| Theme option saved | − | − | − | − | ✓ |
| Post trashed | ✓ | ✓ | ✓ | ✓ | − |
| Deploy | − | − | − | − | ✓ |
| Nightly sync | − | − | − | − | ↗ |
After: edits purge their neighbours, menus and options purge everything, a deploy purges everything, and the sync issues one wildcard purge instead of one per event.
The sync uses one wildcard invalidation instead of one request per event. The map records what each hook does; it does not say that every change requires every target.
One crawler, one redirect, eight percent of a homepage.
One incident deserves the full sequence, because it is the purest example of a key that disagreed with the origin, and because a healthy origin did not mean every edge location served the right answer.
- A crawler asks for a junk legacy URL
/?utm_old=... on the homepage path, one of 95 such requests from 73 addresses that month.
- WordPress answers with a canonical redirect
A 301 to the clean homepage. Correct behavior for one request.
- The edge stores it under the bare homepage key
The policy ignored query strings in the key but forwarded them to the origin. The 301 was filed as the homepage, for 86,400 seconds.
- Eight percent of visitors loop
At the affected edge locations the homepage redirected to itself. Two visitors looped 378 and 150 times. Every monitor stayed green because the monitors asked a different edge.
The six cache incidents in the record
02Logins failing with 403 after a routine CDN configuration change03Event listings up to 11.6 hours stale while the CDN purge count reached about 270 a day04Content changes silently stopped reaching visitors09A page fix deployed, invalidated and confirmed fresh, yet the page kept rendering wrong10A homepage redirect loop for some visitors while every monitor stayed green11Edits and releases invisible to visitors for up to a dayYou are reading the example.
This handbook is served by the system it describes. Here is what we verified in the deployed configuration and the responses from udx.io.
CloudFront before WordPress.
The CDN handles public requests first. HTML and the ?json layout response are distinct variants, so publishing has to invalidate both.
Linux, WordPress, React.
WordPress runs in Linux containers on Kubernetes. The theme renders HTML, then its React front end loads the page layout as JSON.
Memcached behind the page.
A persistent WordPress object-cache drop-in reduces repeated database work. It is a different cache from CloudFront and has a separate invalidation lifecycle.
Files outside the app.
WP-Stateless stores media in Google Cloud Storage; Imgix delivers image variants. The WordPress container does not need to resize every image for every visitor.
Eight decisions to make deliberately.
Use these as starting decisions, then test them against the application and its freshness requirements. The deployed setup on udx.io is documented separately above.
Start around 60 seconds where freshness matters. Configure the CDN minimum, default and maximum TTL deliberately, then verify the headers and observed expiry.
One year, immutable. The name changes when the content does, so the lifetime can be long.
Honor the image service's cache policy. Use versioned URLs or explicit invalidation when an image changes at the same address.
Include the attributes that actually change the response: host, path and meaningful variants. Keep forwarding consistent with the key.
Bypasses the page cache entirely. The object cache still applies.
Batch invalidations and cover the HTML plus its JSON variant. Map edits, menus, options and releases to every affected URL.
Share a coherent cache across application replicas where appropriate, with separate namespaces or services per environment. Invalidate deliberately; do not rely on a flush in just one pod.
The exact requests the application makes, from outside the network, not a human in a browser on the office connection.
A longer TTL can be appropriate when invalidation is dependable. Verify the deployed policy, not just the intended settings in a repository.
Asked at two in the morning.
Do I need a caching plugin if I have a CDN?
A CDN can cache anonymous HTML if configured for it; many CDNs cache only static assets by default. An origin page cache can still reduce work on misses. A persistent object cache helps repeated database reads, including requests that cannot use a shared page cache. Choose the layers from the traffic and freshness requirements, then verify which requests each actually serves.
Why do I still see old content after clearing the cache?
Check which response is stale before clearing everything. Compare the HTML, the ?json layout variant and response headers. A per-pod object cache may disagree with its siblings; a browser may still hold a response under its own lifetime. Purge the affected variants and verify from outside the application network.
What is a cache key and why does it matter?
The key is the set of request attributes the cache uses to decide whether two requests are the same. If the origin varies a response on something the key ignores, one visitor's answer is served to another. If the key includes something the origin ignores, such as a tracking parameter, the cache fragments into copies that never hit. The key must match exactly what the origin varies on, and nothing else.
Redis or Memcached for the WordPress object cache?
Either can work. Coherent invalidation, environment isolation and observability matter more than the brand. This site uses a Memcached-backed WordPress object-cache drop-in. For a replicated application, verify whether the cache is actually shared or local to each replica before assuming one flush reaches everything.
How long should WordPress pages be cached at the edge?
A short lifetime such as sixty seconds is a useful starting point for public HTML when freshness matters. Longer lifetimes require reliable invalidation for every dependent page. The deployed default page policy on udx.io currently uses a one-day TTL; our publishing workflow explicitly invalidates affected paths. That is the current implementation, not the sixty-second recommendation.
Clearing the cache and still seeing the old page?
Tell us what changed, what you purged and what visitors still see. Talk with an engineer about cache invalidation, release reliability and the work needed to fix them.