You hit publish. The post is perfect at its own URL: right title, right date, right image. Then you open the homepage and it is not there. The category archive lists the same five posts it listed yesterday. The blog page shows an excerpt you rewrote an hour ago.
Nothing is broken. Almost every page cache follows one rule: when a post changes, clear the cache for that post’s URL. That ignores the fact that a post also appears on the homepage, on every category and tag archive it belongs to, on the post type archive, and in the feed. Those pages were not edited, so nothing clears them, and they keep serving the copy they hold until it expires.
By the end of this you will be able to prove whether it is the cache, clear the right layer, and set things up so the listing pages update on their own. One section near the end is for people on 20i hosting only, because their server cache has exactly this behaviour. Everything before it applies on any host.
Before you start
You need admin access to WordPress, ideally your hosting control panel so you can see what the host caches, and the CDN dashboard if one sits in front of the site. A CDN is a network that keeps copies of your pages on servers around the world and serves visitors from the nearest one, Cloudflare being the one most people have.
Nothing in the diagnosis is risky. Clearing a cache is safe, and the worst outcome is the next visitor waiting while a page rebuilds. Editing .htaccess or wp-config.php deserves care, so keep a copy of any file before you change it and take a backup on a site you cannot have down.
You will see the word purge a lot below. It only means telling a cache to throw away the copies it is holding, so the next visitor gets a page built fresh.
First, write down every layer that could be caching: your browser, a CDN, a server-level cache at the host, and a plugin inside WordPress. Plenty of sites run three at once without anyone realising.
Step 1: Confirm it really is a cache
Open the homepage in a private window. If the post appears there but not in your normal browser, it is your browser holding an old copy and nothing on the server is wrong.
If it is missing there too, load the homepage with a bit of junk added to the end of the address, so https://example.com/?cachetest=1. That extra part is called a query string, and it makes no difference to what the page shows. Most caches treat any address they have not seen before as a new page, so that request goes all the way through to WordPress and gets built fresh. If the post appears on that version and not on the clean URL, the page itself is fine and something is serving a stored copy.
Now look at the response headers. Those are the notes a server sends back alongside the page, invisible in the browser window, and they name the layer that stored it. The command below is one for the terminal, and if that is not somewhere you go, you can read the same headers in your browser: press F12, open the Network tab, reload the page, and click the first request in the list.
# -s quiet, -S still report errors, -I headers only. curl ignores your own
# browser cache, so this is the server's answer rather than yours.
curl -sSI https://example.com/ | grep -iE 'cache|age|via|expires|x-'
age is the number of seconds since that copy was made, so an age of 40000 on a homepage tells you nobody has purged it today. x-cache: HIT, cf-cache-status: HIT and x-litespeed-cache: hit each name the layer that answered. Run it twice: if age climbs, the same stored copy is being reused.
One last check. Nearly every page cache is bypassed for logged in users, so if the homepage is right logged in and wrong logged out, that is a cache rather than a query problem.
Step 2: Rule out the causes that are not the cache
These produce an identical symptom and take five minutes to eliminate.
The post is scheduled, not published
A future date means the post is behaving correctly. A past date with the status reading “Missed schedule” means WP-Cron never fired. WP-Cron is the timer WordPress uses for jobs that should happen later, and it has a quirk: it only ticks when somebody loads a page, so a quiet site can sail past a publish time with nobody there to trigger it. Republish by hand.
The status or visibility is wrong
A draft, pending or private post renders fine for an administrator and is invisible to everyone else, archive queries included. Check the Status and Visibility panel rather than trusting that the URL loads for you.
Your homepage is a static page
Settings, Reading lets you set a static front page and a separate posts page. A static front page never lists posts unless the template does so deliberately, so check whichever page is set as the posts page.
Sticky posts and the posts per page count
Sticky posts are pinned to the top and push everything else down. With posts per page set low, a new post can land on page two immediately. Look there before deciding it is missing.
The listing query is filtered
Something may be changing the list of posts before the page is built. That could be a pre_get_posts filter in your theme or a plugin, which is code that edits the question WordPress asks the database, or a page builder widget running its own query with a category left out, or a featured posts module pulling from a fixed hand-picked list. All three leave your post out on purpose, and purging fixes none of them.
A CDN is caching your HTML
Cloudflare does not cache HTML by default, but a cache rule or an automatic optimisation feature turns it on, and then clearing WordPress achieves nothing because visitors never reach WordPress. On an HTML response, cf-cache-status: DYNAMIC means it is not caching, HIT means it is.
Step 3: Purge by hand, in the right order
Purge from the inside out. Clear the CDN first and it goes straight back to your actual server (the origin) for a replacement, is handed the stale copy the host’s cache is still holding, and stores that. Now you have a brand new copy of the old page, which is worse than where you started.
So: the WordPress caching plugin, then the host’s server-level cache from the hosting panel, then the CDN. Test in a private window after each. Whichever step fixes the homepage names the layer whose settings you change next.
If your plugin offers clearing and preloading, clear first and preload second. Preloading rebuilds pages by requesting them, so running it first just refills the cache with stale content.
Step 4: Make the cache clear related pages automatically
Purging by hand after every publish is a chore you will forget within a fortnight. The fix is telling the cache which other pages to clear when a post changes.
Every serious caching plugin has this setting under a different name: a purge policy, an “additional pages to purge” list, or checkboxes for what to clear on update. WP Rocket, W3 Total Cache, LiteSpeed Cache, WP Super Cache and WP Fastest Cache all expose a version of it. Make sure the list covers the front page, the posts page, category and tag archives, custom taxonomy archives, the post type archive and the main feed.
If your cache has no such setting, work out the URLs yourself. Which pages list a post depends on its type and its taxonomies, taxonomy being the general word for categories, tags and anything else you group posts by. The snippet below does not purge anything on its own. It works out the list of affected addresses and hands it over, which is the part that is the same on every site:
<?php
/**
* Plugin Name: Related Page Purge
* Description: Lists the pages a post appears on, so they can be purged too.
*/
add_action( 'transition_post_status', 'abcode_purge_related', 10, 3 );
function abcode_purge_related( string $new_status, string $old_status, WP_Post $post ): void {
// Act when a post becomes public, or was public and has now changed.
if ( 'publish' !== $new_status && 'publish' !== $old_status ) {
return;
}
// Revisions and autosaves fire this hook too, and change nothing public.
if ( wp_is_post_revision( $post ) || wp_is_post_autosave( $post ) ) {
return;
}
$urls = array( home_url( '/' ) );
// The blog page, a different URL from the front page when Settings >
// Reading uses a static front page.
$posts_page = (int) get_option( 'page_for_posts' );
if ( $posts_page ) {
$urls[] = get_permalink( $posts_page );
}
// The post type archive, for example /news/ on a custom post type.
$archive = get_post_type_archive_link( $post->post_type );
if ( $archive ) {
$urls[] = $archive;
}
// Every term the post belongs to, across every taxonomy it uses.
foreach ( get_object_taxonomies( $post->post_type, 'names' ) as $taxonomy ) {
$terms = get_the_terms( $post, $taxonomy );
if ( ! is_array( $terms ) ) {
continue;
}
foreach ( $terms as $term ) {
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
$urls[] = $link;
}
}
}
$urls[] = get_feed_link();
// Hand the finished list to whichever cache you run.
do_action( 'abcode_purge_urls', array_values( array_unique( $urls ) ) );
}
The last line announces the list rather than purging it, because every cache clears pages in its own way. That announcement is called a hook, and it means other code can listen for the list and act on it. Check your caching plugin’s developer documentation for the function it offers for clearing one URL, then listen for abcode_purge_urls and call that function once per URL. This last bit of wiring is genuinely a developer job, and it is the only part of this article that is.
Put this in a small site-specific plugin, saved in its own folder under wp-content/plugins, rather than in your theme’s functions.php file, because a theme update overwrites functions.php and takes the snippet with it. The risk is low: the worst a misfire does is purge more often than needed, which costs a little performance and breaks nothing, and deactivating the plugin undoes the lot. If wiring it into your setup is further than you want to go, custom plugin work is something I do regularly.
Step 5: CDN purge rules and choosing a time to live
If your CDN caches HTML, it needs its own instruction. On Cloudflare, the official WordPress plugin has an Automatic Cache Management setting that purges the URLs associated with a post whenever you add, edit or delete one. Without it, you are purging one address at a time through Cloudflare’s own controls, or accepting that the CDN holds pages until their time to live expires. Time to live, often shortened to TTL, is simply how long a cache is allowed to keep a copy before it has to fetch a new one.
The instinct on time to live is usually wrong. When pages go stale, people cut it to fifteen minutes and call it fixed. That is not fixed, it is hidden: most of the benefit of caching has gone so the mistake corrects itself sooner. A long life plus a purge that actually fires gives you fast pages and fresh content.
The exception is HTML in the browser, which should be short lived, because a visitor’s browser is the one cache you cannot purge. Assets are different, since sensible themes add a version string to CSS and image URLs and change it when the file changes. If that is opaque on your setup, it is the sort of thing I untangle during speed optimisation work.
For 20i hosting only: StackCache and the pages it leaves behind
Skip this unless your site is on 20i, or with a reseller who uses 20i, and StackCache is switched on. It applies to nothing else. On Cloudways, SiteGround, Kinsta, WP Engine or anywhere else, the five steps above are your fix.
StackCache is 20i’s server-level page cache. It is quick and it works, and it does what the top of this article describes: publish or edit a post, it clears that post’s own URL, and it stops, so every page that lists the post stays stubbornly old. Clearing StackCache from your 20i control panel after publishing costs nothing and the listings rebuild on the next visit. If you publish twice a month, do that and read no further.
I publish more often, so I wrote a small free plugin: ABCode Improved Caching for 20i StackCache, also on wordpress.org. It widens the purge.
- Purges the homepage, the category and tag archives, the post type archive and the main feed on every edit, not just the post’s own URL.
- Runs those purges in the background after the save, so you never wait on the editor.
- Adds “Purge This Page” to the admin bar while you browse the site, and “Purge Everything” anywhere in the admin, both nonce protected so a stray link cannot fire them.
- Hands the purged pages to ABCode Cache Warmer, if you run it, to be rebuilt straight away.
It is not a caching plugin and will not make an uncached site faster, it knows nothing about Cloudflare or any other CDN, and without StackCache underneath it it does nothing at all, deliberately, and says so on screen rather than looking installed and useful. On 20i with StackCache, this is the gap it was built for: publish a post and the pages that list it clear along with it.
How to check it worked
Change the title of an existing published post to something obviously different, save, and load the homepage in a private window. The new title should be there on the first load, without you clicking anything. Check the category archive, the posts page and /feed/, which is cached separately on a lot of setups and forgotten by nearly every purge policy.
Run curl -sSI on the homepage again. Straight after the save, age should be at or near zero, or the header should read MISS. If age is still in the thousands, the purge never reached that layer. Put the title back afterwards.
When it does not work
The homepage updates but the category archive does not
The purge list covers the front page and stops. Add the term archives explicitly. A post in several categories needs all of them clearing, which is why generating the list in code beats maintaining URLs by hand.
It works logged out but is still wrong logged in
That is not the page cache, since caches skip logged in users. Look instead at the object cache or a transient. Both are places WordPress stores the answer to a database question so it does not have to ask again, and something has kept the old list of posts rather than the old page.
Everything comes right on its own after an hour
Your time to live is doing the work and the purge is not firing at all. Go back to Step 3 and identify the caching layer properly, because the purge is wired to the wrong one.
Scheduled posts only appear once you visit the admin
WP-Cron runs only when the site gets a request, so a quiet site publishes late. The fix is to turn WP-Cron off in wp-config.php and have the server itself call wp-cron.php every few minutes instead, which most hosting panels set up under “Cron Jobs”. That way the timer keeps ticking whether or not anyone is on the site. Worth fixing properly, since the same quirk delays backups and updates, and it is part of what a maintenance plan covers.
Common questions
Why does the post look fine at its own URL but not on the homepage?
Because the post’s URL is the only one your cache was told to clear. The pages that list it were not edited, so as far as the cache is concerned they are still valid.
How long should I wait before assuming something is wrong?
Long enough for your cache’s time to live to pass, usually an hour to a day. If it corrects itself after that, the content was never the problem and the purge is.
Do I need a caching plugin if my host already caches?
Usually not for page caching, and running both is a common cause of confusing behaviour. A plugin can still add browser cache rules, object caching and a better purge policy.
Getting the listing pages to keep up
The awkward part of this problem is how convincingly it imitates a broken site. Nothing errors, nothing logs, the post is exactly where you put it, and visitors are still being shown yesterday’s news. Once you know that caches clear the page you edited and not the pages that mention it, it becomes ordinary.
Confirm with a private window and a query string, rule out scheduling, status and query filters, purge from the inside out, then spend twenty minutes on the purge policy so you never do it by hand again. On 20i, the plugin above covers it. Everywhere else, the setting is in your cache plugin and it is worth finding. If you are still stale after all that, or you would rather someone else worked out which of the four caches on your stack is lying to you, get in touch and I will take a look.