I'll show you how to nginx cache I purge it selectively, without serving visitors outdated responses or risking security vulnerabilities. Using clear purge strategies, clean cache keys, and secure automation, I build a Workflow that keeps WordPress and PHP-FPM running quickly and up to date.
Key points
- Cache keys Plan Carefully: Host, URI, Headers, and Required Cookies
- Purge Strategies Combine: Expiration times, specific keys, controlled "Purge All"
- Security Prefer: internal IPs, authentication, logging, no open endpoints
- Automation Use: WordPress Hooks and Deployment Triggers for Purges
- Monitoring Enable: X-FastCGI Cache, Logs, Cache Sizes
Understanding NGINX Caching: The Basics of Effective Purging
Before I purge, I understand how NGINX stores. NGINX serves HTTP backends via a proxy cache and dynamic PHP responses via a FastCGI cache; there are also options such as uWSGI or SCGI for specific setups, which I’ll only touch on briefly here. In typical WordPress or PHP stacks, the FastCGI Cache has the greatest impact because it writes rendered HTML pages from PHP-FPM to the file system and serves them directly the next time they’re requested. This reduces the load on the CPU and database and shortens response times, as long as the content is up to date. This is precisely where smart purging determines whether users receive up-to-date responses or see outdated pages.
Cache Keys: The Key to Targeted Purging
Each hit is based on a Cache key, which usually consists of the host, request URI, relevant headers, and minimal cookie data. I design the key so that it only accounts for differences that actually alter the HTML output; otherwise, I fragment the cache unnecessarily. I use Vary headers, language, or device classes sparingly and use test requests to verify whether the desired variation is truly needed. A consistent key makes it possible later to remove only those objects affected by a change, rather than deleting entire directories. Clean keys save I/O, keep the hit rate high, and make it easier to Purge- Requests are immense.
Cache Key Design in Practice: Normalization and Reduction
In practice, I consistently normalize the key: unnecessary query parameters are removed, only a few whitelisted parameters are retained, and cookies are included in the key only if they visibly alter the HTML output. This way, I prevent tracking parameters like utm_* or fbclid from generating thousands of variations of the same page.
# Cache Zone and Headers
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=FCGI:256m inactive=60m max_size=10g;
map $http_cookie $no_cache {
default 0;
~*wordpress_logged_in 1;
~*comment_author 1;
~*woocommerce_items_in_cart 1;
}
# Cache only GET/HEAD requests; never cache POST requests
map $request_method $cache_method_ok { default 0; GET 1; HEAD 1; }
# Whitelist query strings: e.g., pagination and search
map $arg_page $qs_page { "" ""; default "page=$arg_page"; }
map $arg_s $qs_s { "" ""; default "s=$arg_s"; }
# Suppress empty parts and concatenate
map "$qs_page$qs_s" $qs {
"" "";
default "?$qs_page$qs_s";
}
# Query string-free path
map $request_uri $path_noargs { ~^([^?]+) $1; }
# Consistent cache key
set $my_cache_key "$scheme$host$path_noargs$qs";
server {
# ...
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
# Cache Settings
fastcgi_cache FCGI;
fastcgi_cache_key $my_cache_key;
fastcgi_cache_methods GET HEAD;
fastcgi_no_cache $no_cache;
fastcgi_cache_bypass $no_cache;
add_header X-FastCGI-Cache $upstream_cache_status always;
}
}
I hold the No-Cache-Strict rule: Registered users, shopping carts, and comment authors bypass the cache; anonymous readers continue to benefit. For device classes or languages, I make a conscious choice: If CSS/JS already handles this responsively, I omit a variation in the key, thereby increasing the hit rate.
Why Targeted Purging Is Crucial
Content is constantly changing: new posts, revised menus, redesigned homepages, or template changes that alter HTML structures—and that’s exactly when I want to control what the cache serves. Without a targeted purge, NGINX serves old files until they expire, which in extreme cases can take days and result in incorrect information for readers. With a controlled purge, I discard only what really needs to be re-rendered, keep the caches warm, and save Server load. I'd rather plan changes with broader implications in a short Optimization Window, so that the server doesn't experience load spikes when it's being repopulated. This keeps the page fast and prevents visual errors that often stem from outdated HTML or JSON responses.
Cache Invalidation Strategies: Sequential, Key Purge, and Complete Wipe
I combine three methods for effective caching: expiration times for content that naturally becomes outdated, targeted key purging for specific URLs, and a complete wipe of a zone following structural changes. I set short expiration times for highly dynamic pages and longer ones for static landing pages, so that Hit rates remain high. I trigger the key purge as soon as a post or menu item is saved, and include not only the individual URL but also any affected archives or the homepage. I reserve the full wipe for template changes, major plugin modifications, or cache corruption. The following table helps me quickly choose the right approach and realistically assess the risks.
| Strategy | Control system | Strengths | Risks | Typical use |
|---|---|---|---|---|
| Expiration | inactive, max_age | Little effort | Outdated Content Until Expiration | Archive pages, pages that are rarely updated |
| Key Purge | Specific URL/Key | Granular and fast | Incorrect keys do not work | Post Update, Menu Change |
| Wildcard Purge | Prefix with * | Group Deletion | Deleted too much | Series, Category Clusters |
| Purge All | Clear Zone | Unified Restart | High Load During Refill | Changing Templates/Themes |
FastCGI Cache on the File System: Setup, Zones, and Limits
I configure the FastCGI cache using fastcgi_cache_path Set it up, define a clear storage location (e.g., /var/cache/nginx/fastcgi), choose levels such as 1:2 for flat directories, and assign a keys_zone with a descriptive name and appropriate size. The inactivity timeout and an upper limit prevent the cache from becoming too large and keep the SSD running smoothly. NGINX stores hash files here that are nearly impossible to map manually without tools; that’s why I plan in advance how I’ll delete them: individual keys via modules or scripts, and entire zones via systematic commands. For WordPress stacks, this setup pays off in the form of a measurably reduced TTFB, especially for uncached first-time requests following deployments. Those interested in delving deeper into performance tuning will find additional ideas at WordPress Speed and can link them to its own purge rules.
Prevent Stampedes and Make Good Use of Stale Data
During purging or after timeouts, there must not be a sudden surge of requests to PHP-FPM. I therefore enable locks and stale strategies: The first request rebuilds the object, while concurrent requests wait briefly (lock), and in the event of errors or timeouts, I retrieve from a defined set (use_stale). Background updates keep hot paths up to date without slowing down readers.
#: Avoid Cache Storms and Take Advantage of Grace Periods
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
fastcgi_cache_lock_age 10s;
fastcgi_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
fastcgi_cache_background_update on;
# Recommended Default Times
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_valid 404 1m; # Keep errors in the cache only briefly
This helps me reduce CPU spikes and prevents brief backend glitches from slowing down entire sections. This safeguard is especially valuable during large purges or deployments, because it keeps warm-up phases controlled and predictable.
Safe Purge: Scripts, Checks, and Careful Scope
On production systems, I only run purge scripts with root run and ensure strict path validation so that no incorrect directories are deleted. Before removing anything, my script checks whether the target-path variable is set and mounted to the expected cache directory; otherwise, it aborts. I provide the tool with two modes: targeted key purge (file by hash) and controlled clearing of the zone, for which I require additional confirmation. Logs record each deletion with a timestamp so that I can clearly trace cause and effect later. The less I delete globally, the faster the cache stays “warm,” and that’s exactly what my Strategy from.
HTTP Purge via Modules: Targeted, Automated, and Traceable
If there is no native interface, I use an add-on module such as ngx_cache_purge and handle PURGE requests via a custom location that uses the same Cache key Calculated like GET. The module removes entries for individual URLs, can delete groups using wildcards, and, as a final step, empties an entire zone. Applications like WordPress automatically trigger purges for post URLs, the homepage, and relevant archives after a post is saved, ensuring content remains up-to-date without manual intervention. I strictly limit wildcards to unique prefixes because overly broad patterns remove an unnecessary number of objects. For particularly short-lived content, it’s also worth using a Microcaching Approach, which intelligently combines second-level caches with PURGE.
Secure Access to Purge Endpoints
Whenever an HTTP endpoint exists, I secure it thoroughly: Access is allowed only from internal IPs such as 127.0.0.1 or an admin VPN address, plus HTTP authentication with a strong password. I use non-obvious path names, log every PURGE request, and limit the rate to prevent waves of requests from unintentionally hitting the backend. The location allows only the PURGE method and GET for status queries; I block everything else. This prevents abuse and allows me to see immediately in the log which application invalidated which URL and when. Security takes precedence over convenience here, because an open endpoint can Attacks invite.
WordPress Integration: Hooks, Target URLs, and Caching Logic
In WordPress, I attach Purges to hooks that fire when changes are made—for example, when a post is saved or a menu is restructured. The hook triggers requests for all directly affected URLs: individual posts, the first page of the category, the homepage, and, if available, relevant tag archives, so that visitors immediately see the correct content. I avoid global purges for minor edits; otherwise, the benefit of a warm cache is lost and the Response times vary. For multilingual and personalized sections, I clearly separate which cookies actually affect the HTML output so that the key isn't unnecessarily fragmented. With a clear purge list and sparing use of wildcards, the system remains fast while also staying reliably up to date.
WordPress Examples: Hooks, URL Selection, and Rollback
To ensure clean purges, I define a small but complete set of URLs for each event. When saving a post, this includes at least: the post’s permalink URL, the homepage (if it displays recent posts), the first category page, tag archives (if applicable), and JSON feeds. For menus, this also includes all pages that display the menu (often globally: the homepage, archive pages, and 404 pages).
// Pseudocode: Purge targets after a post update
on save_post($post_id) {
$urls = [
get_permalink($post_id),
home_url('/'),
get_category_link(primary_category($post_id)),
get_tag_link(primary_tag($post_id)),
home_url('/feed/'),
];
purge_urls(array_unique(array_filter($urls)));
}
// Purge handler calls the secure PURGE endpoint
function purge_urls($urls) {
foreach ($urls as $u) {
http_request('PURGE', internal_purge_endpoint($u));
}
}
I keep an eye on rollback cases: If a status changes from "Draft" to "Published" or back again, I update the purge list accordingly (archive pages, home page). For bulk changes (imports, term renamings), I batch the purges and spread them out over short time windows to avoid traffic spikes.
Handling E-Commerce and Session Cases Correctly
Shops and other session-heavy areas require strict rules: The shopping cart, checkout, and account/login pages must not be cached. I control this using cookie patterns (e.g., `woocommerce_items_in_cart`), precise URL matches (`/cart`, `/checkout`, `/my-account`), and set fastcgi_no_cache and bypass On the other hand, product detail pages are excellent for caching, as long as price and inventory information does not vary by user. For short-lived notifications (e.g., „Added to Cart“), I handle this on the client side and keep HTML variations minimal.
Best Practices for Productive Environments
I start with a clear Cache strategy: Short cache lifetimes for the front page, blog index, or store listings; longer lifetimes for static pages and documentation. I then define purge rules that only delete specific items when content changes, while deployments trigger a controlled, larger-scale purge. I include the X-FastCGI-Cache: HIT, MISS, or BYPASS header in every delivery so that I can see in the browser or via curl what actually came from the cache. I monitor the cache zone based on size and number of files so that I can identify bottlenecks and adjust limits in a timely manner. For assets like CSS and JS, I use versioning in filenames, which often makes purges unnecessary for static files and the Traffic decreases.
Hosting Options: Shared, Managed, and Dedicated Server
In shared hosting environments, I usually control purges via a control panel or plugin because I don’t have direct access to NGINX, and this allows me to keep the cache up to date. Managed WordPress hosts often integrate caching deeply into their platform; in these cases, I follow their guidelines and check how automatic purges are linked to CMS events. On a VPS or dedicated server, I take full control: configuration, Scripts, endpoints, security, and monitoring. This level of control is worthwhile for high traffic and a large number of editors, as it allows me to carefully balance performance and timeliness. Those who prefer a robust platform with good NGINX caching can check out offerings like webhoster.de and apply the workflows described here directly.
Prewarming After Purges: Controlled and Resource-Efficient
After targeted purges, I deliberately warm up hot paths instead of making visitors bear the cost. I do this with a small script that sequentially calls important URLs and pauses in between. In doing so, I pay attention to HEAD/GET requests, HTTP/2 connectivity, and low concurrency to ensure that PHP-FPM doesn’t get overwhelmed.
# Example: Warmup Using a List of URLs
#!/bin/bash
URLS=("https://example.com/" "https://example.com/blog/" "https://example.com/kategorie/foo/")
for u in "${URLS[@]}"; do
curl -s -I "$u" >/dev/null
sleep 0.2
done
For larger sites, I generate the list from sitemaps or CMS exports, group them into batches, and spread the warm-up job across minute-long windows. During deployments, I start the prewarming shortly after the targeted purges so that readers are served quickly during peak times.
Enable Monitoring and Logging
Transparency is essential. I'm expanding the NGINX log format to include cache status and separating access logs from purge logs. This allows me to identify patterns (such as frequent BYPASS events caused by cookie rules or a spike in MISS events following deployments) and fine-tune limits accordingly.
# Access Logs with Cache Status
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time '
'cache=$upstream_cache_status';
access_log /var/log/nginx/access.log main;
# Example Analysis
# grep 'cache=HIT' /var/log/nginx/access.log | wc -l
# grep 'cache=BYPASS' /var/log/nginx/access.log | wc -l
I also monitor the cache zone (number of files, bytes), inode usage, and I/O values. If the hit rate drops, I first check: Has the key changed unintentionally? Are there too many cookies involved? Have new query parameters been introduced? Or are bypass rules unexpectedly blocking traffic?
Multi-site and multi-domain environments
For networks with multiple domains, I separate zones or neatly encapsulate them by host in the key. For particularly large tenants, I use my own keys_zone-Entries, so that hot sites don't take up all the storage space. I orchestrate the purging on a per-site basis: The WordPress hook decides locally which URLs to invalidate, and the endpoints are secured identically. I strictly separate staging and production using different zones/directories to prevent cross-purges from occurring.
Avoiding Error Scenarios: From Bypass to Purge All
Many problems arise from overly broad bypass rules that, in certain Cookies completely bypass the cache and ruin the hit rate. I keep exclusions to a minimum and use test accounts to check whether personalization truly requires server-side rendering or can run via JavaScript. A permanent “Purge All” slows down every page, so I only use it after structural changes and outside of peak traffic times. A lack of transparency hinders troubleshooting, so I enable clear headers and logs from the start and test changes in a reproducible manner on staging. If there are no hits, I examine keys, check response headers, compare host/URI normalization, and look at cache sizes as well as Inactivity-Timer.
Summary in brief
With a clean Cache key, smart expiration times, and targeted purges, I keep pages fast and content accurate. Scripts with path checks and strict endpoint security prevent misuse and avoid accidental data deletions. WordPress hooks provide the right automation without clearing the entire cache for every little thing. Monitoring via X-FastCGI-Cache, logs, and zone sizes shows me where I need to fine-tune settings and whether my workflow is effective. Those who take these points to heart can combine high speed with reliable up-to-date content—the foundation for smooth Delivery on any PHP-based website.


