Full-page caching in WordPress can significantly speed up your site by serving pre-built HTML pages directly, bypassing much of WordPress’s usual processing. While plugins make this easy, you can achieve it without one using Nginx and PHP for a more customized and potentially more performant solution. This involves Nginx serving cached files when available and PHP generating them when not, all with some clever configuration.
Full-page caching, sometimes called “page caching,” works by storing the complete HTML output of a webpage. When a user requests that page again, instead of WordPress rebuilding it from scratch (which involves database queries, PHP execution, and theme rendering), the pre-generated HTML is served instantly. This drastically reduces server load and improves response times.
Why Cache?
WordPress is dynamic, which is great for flexibility but not always for speed. Each page load typically involves:
- Database queries: Retrieving posts, comments, settings, and more.
- PHP execution: Running theme functions, plugin code, and core WordPress logic.
- File I/O: Loading theme files, plugin files, and images.
Caching bypasses most of these steps for subsequent visitors, delivering a much faster experience.
Why No Plugin?
While plugins like WP Super Cache or W3 Total Cache are popular, going plugin-less offers a few advantages:
- Less overhead: Plugins add their own code, database entries, and potential conflicts. A custom Nginx/PHP solution is leaner.
- Fine-grained control: You dictate exactly what gets cached, how it’s invalidated, and exclusion rules.
- Performance: Nginx is incredibly efficient at serving static files, making it an excellent cache server.
The main downside? It requires more technical know-how to set up and maintain.
If you’re looking to enhance your WordPress site’s performance through full-page caching without relying on plugins, you might find the article on How to implement full-page caching in WordPress without a plugin — using Nginx and PHP particularly useful. This guide provides a step-by-step approach to configuring Nginx and PHP for optimal caching, ensuring that your site loads faster and provides a better user experience. Additionally, it may be beneficial to explore related topics on server optimization and performance tuning to further improve your website’s efficiency.
Nginx as Your Caching Proxy
Nginx (pronounced “engine-x”) is a powerful web server often used as a reverse proxy, load balancer, and HTTP cache. It’s ideally suited for serving static content quickly, making it perfect for our caching strategy.
Nginx’s Role in Caching
Instead of passing every request directly to PHP, Nginx will act as a gatekeeper. When a request comes in:
- Nginx checks if a cached version of the page exists.
- If it does, and it’s valid, Nginx serves it directly without bothering PHP.
- If not, Nginx passes the request to PHP.
- PHP processes the request, generates the HTML, and then “tells” Nginx to save a copy of this output.
Setting Up Nginx Cache Zones
First, you need to tell Nginx where to store cached files and how to manage them. This is done with a proxy_cache_path directive, usually placed in your Nginx configuration’s http block (e.g., /etc/nginx/nginx.conf or a custom configuration file it includes).
“`nginx
/etc/nginx/nginx.conf or included file
http {
… other http configurations …
Define a cache zone
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=wp_cache:10m inactive=60m max_size=1G;
Define a key for your cache (important for identifying cached content)
proxy_cache_key “$scheme$request_method$host$request_uri”;
… more http configurations …
}
“`
Let’s break down this line:
/var/cache/nginx: This is the filesystem path where Nginx will store its cached files. Make sure Nginx has write permissions to this directory.levels=1:2: This creates a two-level directory hierarchy (e.g.,/var/cache/nginx/c/29/) to store files. This helps prevent too many files in a single directory, improving performance.keys_zone=wp_cache:10m: Defines a shared memory zone namedwp_cacheof 10 megabytes. Nginx uses this to store metadata about cached items (like their keys, if they’re valid, and how many times they’ve been accessed). A 10MB zone can store metadata for tens of thousands of cached items.inactive=60m: Any item in the cache that hasn’t been accessed for 60 minutes will be removed. This helps keep the cache fresh.max_size=1G: The maximum size of the cache on disk. Once exceeded, Nginx will start removing older or less frequently accessed files.
The proxy_cache_key directive is crucial. It defines how Nginx identifies a unique request for caching. Here, we’re using the scheme (http/https), request method (GET/POST), host, and full request URI. This ensures that http://example.com/page/ is cached separately from https://example.com/page/ or http://example.com/page/?foo=bar.
PHP’s Role in Generating and Signaling
Even without a caching plugin, WordPress still processes the request if Nginx can’t find a cached page. Our goal is to have PHP signal to Nginx when a page is cacheable and when it isn’t.
Modifying wp-config.php for Caching Headers
We’ll use PHP to send specific HTTP response headers that Nginx understands. The X-Accel-Expires header is key here. It tells Nginx how long to cache the response.
Open your wp-config.php file and add the following code, ideally before the / That's all, stop editing! Happy publishing. / line.
“`php
// Nginx Full-Page Caching Headers
// Define a cache age for Nginx (in seconds).
// Adjust this based on how often your content changes.
// For static content like blog posts, 24 hours (86400 seconds) is often good.
// For pages that update more frequently, reduce this.
define(‘NGINX_CACHE_AGE’, 3600); // 1 hour
// Send X-Accel-Expires header for Nginx caching
// This tells Nginx to cache the page for NGINX_CACHE_AGE seconds.
// This is done before any output is sent, so it needs to be early in the WordPress lifecycle.
// If not logged in and not an admin, and it’s a GET request, set cache header.
if ( ! defined(‘DOING_CRON’) && ! defined(‘WP_CLI’) && ! defined(‘REST_REQUEST’) && ! defined(‘XMLRPC_REQUEST’) && ! defined(‘DOING_AJAX’) &&
! (defined(‘WP_ADMIN’) && WP_ADMIN === true) && ! (defined(‘WP_SANDBOX_SCRAPING’) && WP_SANDBOX_SCRAPING === true) ) {
// Check if the user is logged in
$user = wp_get_current_user();
if ( is_user_logged_in() && $user->ID !== 0 ) {
// Logged-in users should not receive cached pages, or their cache should be instant expire.
// Option 1: Don’t cache for logged in users (preferred).
header(‘Cache-Control: private, no-cache, no-store, must-revalidate’); // Standard HTTP header to prevent browser caching
header(‘X-Accel-Expires: 0’); // Tell Nginx not to cache or to purge existing cache.
} else {
// Not logged in, eligible for caching
// Set the Nginx cache expiration header.
header(“X-Accel-Expires: ” . NGINX_CACHE_AGE);
}
} else {
// For admin, AJAX, REST, WP-CLI, etc., do not cache
header(‘X-Accel-Expires: 0’);
}
“`
Let’s unpack this PHP snippet:
NGINX_CACHE_AGE: This constant sets the default cache duration in seconds. Adjust this value based on your site’s content update frequency.- The
ifcondition checks several things: ! defined('DOING_CRON'): Prevents caching of cron job requests.! defined('WP_CLI'): Prevents caching during WP-CLI operations.! defined('REST_REQUEST'): Prevents caching for WordPress REST API requests.! defined('XMLRPC_REQUEST'): Prevents caching for XML-RPC requests.! defined('DOING_AJAX'): Prevents caching of AJAX requests.! (defined('WP_ADMIN') && WP_ADMIN === true): Ensures we don’t cache the WordPress admin area.! (defined('WP_SANDBOX_SCRAPING') && WP_SANDBOX_SCRAPING === true): Another check for sandbox scraping.- Inside the main
if, we checkis_user_logged_in(). This is crucial: - Logged-in users: WordPress usually shows personalized content to logged-in users (e.g., “Hello, Admin!”). Caching pages for them would show static content meant for anonymous users. Therefore, we send
X-Accel-Expires: 0to tell Nginx not to cache this response or to immediately expire any existing cache related to this key. We also add standardCache-Controlheaders for browsers. - Anonymous users: For non-logged-in visitors, we send
X-Accel-Expires: NGINX_CACHE_AGEto tell Nginx to cache this page for the defined duration. - The
elseblocks cover all other scenarios (admin, AJAX, etc.), telling Nginx not to cache them by sendingX-Accel-Expires: 0.
Clearing Cache on Content Updates
The X-Accel-Expires header only tells Nginx how long to cache a page. What if you update a post? The old cached version will still be served until its expiration. To fix this, we need to instruct Nginx to purge specific cached items when content changes.
WordPress has actions for when posts are saved, deleted, etc. We can hook into these to send a signal to Nginx.
Add this to your theme’s functions.php or a custom plugin:
“`php
// Nginx Cache Purging Functions
// Function to purge a specific URL from Nginx cache
function nginx_purge_cache_url($url) {
// If the cache purge path is not configured, don’t attempt to purge
if (!defined(‘NGINX_PURGE_PATH’) || !NGINX_PURGE_PATH) {
error_log(‘NGINX_PURGE_PATH is not defined for cache purging.’);
return;
}
$parsed_url = parse_url($url);
if (!isset($parsed_url[‘path’])) {
error_log(‘Invalid URL for cache purging: ‘ . $url);
return;
}
// Construct the purge request URI
// Nginx will intercept requests to NGINX_PURGE_PATH/actual/url/path
$purge_uri = NGINX_PURGE_PATH . $parsed_url[‘path’];
if (isset($parsed_url[‘query’])) {
$purge_uri .= ‘?’ . $parsed_url[‘query’];
}
// Use wp_remote_get for the purge request.
// Ensure this request is made to the actual domain, not localhost, if Nginx isn’t listening for purge on localhost.
// If Nginx is on the same server, an internal request is fine.
// You might need to adjust the host header if your Nginx rules are host-dependent for purging.
$response = wp_remote_get(home_url($purge_uri), array(
‘timeout’ => 5, // Set a timeout for the request
‘sslverify’ => false, // Set to true in production if your site uses SSL
‘headers’ => array(
‘Host’ => $_SERVER[‘HTTP_HOST’] // Ensure the host matches for Nginx processing
)
));
if (is_wp_error($response)) {
error_log(‘Nginx cache purge failed for URL: ‘ . $url . ‘ – Error: ‘ . $response->get_error_message());
} else {
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) { // Nginx purge module usually returns 200 or 404 if not found
error_log(‘Nginx cache purge (HTTP ‘ . $response_code . ‘) for URL: ‘ . $url);
} else {
error_log(‘Nginx cache purged successfully for URL: ‘ . $url);
}
}
}
// Hook into WordPress actions to clear cache for relevant pages
function clear_nginx_cache_on_update($post_id) {
if (defined(‘DOING_AUTOSAVE’) && DOING_AUTOSAVE) return;
if (wp_is_post_revision($post_id)) return;
$post_url = get_permalink($post_id);
if ($post_url) {
nginx_purge_cache_url($post_url);
// Also purge the homepage cache if the post is published and visible.
if (get_option(‘show_on_front’) == ‘posts’ && get_post_status($post_id) == ‘publish’) {
nginx_purge_cache_url(home_url(‘/’));
}
}
// Clear archive caches (e.g., category, tag, author, date archives)
$post_type = get_post_type($post_id);
if ($post_type == ‘post’) {
$categories = get_the_category($post_id);
foreach ($categories as $category) {
nginx_purge_cache_url(get_category_link($category->term_id));
}
$tags = get_the_tags($post_id);
if ($tags) {
foreach ($tags as $tag) {
nginx_purge_cache_url(get_tag_link($tag->term_id));
}
}
// Author archive
$author_id = get_post_field(‘post_author’, $post_id);
nginx_purge_cache_url(get_author_posts_url($author_id));
// Date archives (yearly, monthly, daily)
$timestamp = get_the_time(‘U’, $post_id);
nginx_purge_cache_url(get_year_link($timestamp));
nginx_purge_cache_url(get_month_link(substr(get_the_time(‘Y’, $post_id), 0, 4), substr(get_the_time(‘m’, $post_id), 0, 2)));
nginx_purge_cache_url(get_day_link(substr(get_the_time(‘Y’, $post_id), 0, 4), substr(get_the_time(‘m’, $post_id), 0, 2), substr(get_the_time(‘d’, $post_id), 0, 2)));
}
// Also purge caches for custom post types and taxonomies as needed
// Example for a ‘product’ CPT and ‘product_cat’ taxonomy:
/*
if ($post_type == ‘product’) {
$terms = get_the_terms($post_id, ‘product_cat’);
if ($terms && !is_wp_error($terms)) {
foreach ($terms as $term) {
nginx_purge_cache_url(get_term_link($term, ‘product_cat’));
}
}
}
*/
}
add_action(‘save_post’, ‘clear_nginx_cache_on_update’);
add_action(‘deleted_post’, ‘clear_nginx_cache_on_update’);
add_action(‘clean_post_cache’, ‘clear_nginx_cache_on_update’); // General cleanup hook
// Clear homepage when comments are updated
function clear_homepage_cache_on_comment($comment_id) {
nginx_purge_cache_url(home_url(‘/’));
$post_id = get_comment($comment_id)->comment_post_ID;
nginx_purge_cache_url(get_permalink($post_id));
}
add_action(‘wp_insert_comment’, ‘clear_homepage_cache_on_comment’);
add_action(‘edit_comment’, ‘clear_homepage_cache_on_comment’);
add_action(‘delete_comment’, ‘clear_homepage_cache_on_comment’);
add_action(‘untrash_comment’, ‘clear_homepage_cache_on_comment’);
add_action(‘wp_set_comment_status’, ‘clear_homepage_cache_on_comment’); // For approve/reject
// For menu updates
add_action(‘wp_update_nav_menu’, ‘nginx_purge_all_cache’);
add_action(‘wp_create_nav_menu’, ‘nginx_purge_all_cache’);
add_action(‘wp_delete_nav_menu’, ‘nginx_purge_all_cache’);
// For theme customization changes (might affect header/footer)
add_action(‘customize_save_after’, ‘nginx_purge_all_cache’);
// Function to purge the entire Nginx cache (use sparingly)
function nginx_purge_all_cache() {
// This typically involves sending a request to a special Nginx purge URI
// or deleting the cache directory content directly from the server.
// For now, we simulate by purging the homepage which is often a starting point.
// More robust multi-server solutions might use file system or a dedicated purge module.
nginx_purge_cache_url(home_url(‘/’)); // At least purge the homepage
error_log(‘All Nginx cache potentially purged/refreshed via homepage purge.’);
}
add_action(‘switch_theme’, ‘nginx_purge_all_cache’); // When theme changes
add_action(‘upgrader_process_complete’, ‘nginx_purge_all_cache’, 10, 2); // When plugins/themes are updated
add_action(‘after_switch_theme’, ‘nginx_purge_all_cache’);
add_action(‘update_option_home’, ‘nginx_purge_all_cache’);
add_action(‘update_option_blogname’, ‘nginx_purge_all_cache’); // Site title might be in cache
add_action(‘update_option_blogdescription’, ‘nginx_purge_all_cache’); // Tagline might be in cache
add_action(‘update_option_permalink_structure’, ‘nginx_purge_all_cache’); // Permalink structure changes everything
// You must define NGINX_PURGE_PATH in wp-config.php or similar.
// For example: define(‘NGINX_PURGE_PATH’, ‘/nginx-cache-purge’); for an Nginx purge rule.
// Or define(‘NGINX_PURGE_PATH’, ‘/_this_nginx_does_not_purge_’); if you handle purge differently.
“`
And in your wp-config.php:
“`php
// Define a path for Nginx cache purging.
// This path needs to be configured in your Nginx server block to trigger a cache delete.
// Make sure this path is unique and not something a real page would use.
define(‘NGINX_PURGE_PATH’, ‘/nginx-cache-purge’);
“`
This PHP code uses wp_remote_get() to make an internal HTTP request to a specially configured Nginx location (/nginx-cache-purge/). We’ll set up Nginx to interpret requests to this path as a command to clear its cache for the specified URL. We’re also making sure to clear relevant archive pages and the homepage when content changes.
Configuring Nginx for Caching and Purging
Now we need to tie everything together in your Nginx server block (usually etc/nginx/sites-available/yourdomain.conf).
Main Nginx Server Configuration
Here’s a generalized Nginx server block. You’ll merge this with your existing configuration.
“`nginx
server {
listen 80;
listen [::]:80;
listen 443 ssl http2; # If using SSL
listen [::]:443 ssl http2;
server_name yourdomain.com www.yourdomain.com;
SSL configuration (if applicable)
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/live/yourdomain.com/dhparam.pem;
root /var/www/yourdomain.com/public_html; # Your WordPress root
index index.php index.html index.htm;
Log files
access_log /var/log/nginx/yourdomain.com_access.log;
error_log /var/log/nginx/yourdomain.com_error.log;
Deny access to hidden files
location ~ /\. {
deny all;
}
Deny access to wp-config.php and other sensitive files
location ~* /(?:wp-config\.php|wp-cron\.php|readme\.html|license\.txt)$ {
deny all;
}
Deny access to these files / folders
location ~ /(?:wp-content|wp-includes|xmlrpc\.php|uploads)/.\.php$ {
deny all;
}
Deny access to version control files
location ~ /\.git {
deny all;
}
Cache Configuration for WordPress
location / {
Check if a cached version exists and is valid
proxy_cache wp_cache; # Use our defined cache zone
proxy_cache_valid 200 30m; # Cache 200 OK responses for 30 minutes (fallback)
The X-Accel-Expires header from PHP takes precedence.
proxy_cache_min_uses 1; # Only cache after 1 use (optional, can be 1 immediately)
proxy_cache_revalidate on; # Revalidate stale cache with If-Modified-Since/If-None-Match
proxy_cache_use_stale error timeout updating http_500 http_503; # Serve stale while updating
Add Cache-Status header for debugging
add_header X-Cache-Status $upstream_cache_status;
Do not cache specific requests (based on cookies, query strings, methods)
This is where we tell Nginx NOT to cache certain things even if PHP says ok
if ($request_method = POST) {
proxy_pass http://unix:/run/php/php8.1-fpm.sock; # Your PHP-FPM socket path
break;
}
Don’t cache admin area, login/register pages, or common WordPress cookies
if ($request_uri ~* “/(wp-admin|wp-login.php|wp-register.php)”) {
proxy_pass http://unix:/run/php/php8.1-fpm.sock;
break;
}
if ($http_cookie ~* “comment_author_|wordpress_logged_in_|wp-postpass_|wordpress_no_cache|wordpress_sec_|wordpress_test_cookie”) {
proxy_pass http://unix:/run/php/php8.1-fpm.sock;
break;
}
Default proxy to PHP-FPM for all other requests
try_files $uri $uri/ /index.php?$args;
}
PHP-FPM configuration for dynamic requests
This location block processes all .php requests that aren’t blocked by the above.
location ~ \.php$ {
Check for non-existent files for security
try_files $uri =404;
Pass to PHP-FPM via socket (recommended) or TCP (e.g., 127.0.0.1:9000)
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust PHP version as needed
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
Explicitly disable caching for PHP execution context if not controlled by X-Accel-Expires
In our setup, X-Accel-Expires handles this, so these are mostly for explicit clarity.
fastcgi_cache_bypass $http_cookie;
fastcgi_no_cache $http_cookie;
}
Nginx Cache Purge Location
This block handles requests to the NGINX_PURGE_PATH defined in wp-config.php.
location ~ ^/nginx-cache-purge(/.*) {
Restrict access to this location. Only Allow from localhost/your internal IP.
This prevents external users from purging your cache.
allow 127.0.0.1;
allow YOUR_SERVER_IP; # If your WordPress container/VM IP is different from Nginx host.
deny all;
The actual cache purging directive
proxy_cache_purge wp_cache “$scheme$request_method$host$1”; # $1 refers to the (.*) from the regex
}
Media files caching (optional but recommended for faster delivery)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp)$ {
expires 30d; # Cache these files for 30 days in browser
log_not_found off;
access_log off; # No need to log these requests
}
Font files caching
location ~* \.(ttf|ttc|otf|eot|woff|woff2)$ {
add_header “Access-Control-Allow-Origin” “*”; # Important for CORS
expires 30d;
log_not_found off;
access_log off;
}
}
“`
Key points in the Nginx configuration:
location / { ... }: This is the main block for handling site requests.proxy_cache wp_cache;: Tells Nginx to use our defined cache zone.proxy_cache_valid 200 30m;: A fallback. If PHP doesn’t send anX-Accel-Expiresheader, Nginx will cache 200 OK responses for 30 minutes. The PHPX-Accel-Expiresoverrides this.add_header X-Cache-Status $upstream_cache_status;: This is incredibly useful for debugging. It adds an HTTP header to the response, telling you if the page was aHIT(served from cache),MISS(not in cache, served by WordPress), orBYPASS(Nginx chose not to cache).- Conditional
proxy_passblocks withbreak;: These are crucial for preventing caching of dynamic or private content: if ($request_method = POST): POST requests usually involve data submission (comments, forms, logins) and should never be cached.if ($request_uri ~* "/(wp-admin|wp-login.php|wp-register.php)"): Prevents caching of admin and authentication pages.if ($http_cookie ~* "comment_author_|wordpress_logged_in_..."): This looks for common WordPress cookies that indicate a logged-in user, a commented-on user, or other dynamic states. If any of these cookies are present, Nginx bypasses the cache and passes the request directly to PHP. This is how we ensure logged-in users always see dynamic content.- **
location ~ ^/nginx-cache-purge(/.*)**: This is the dedicated block for cache purging. allow 127.0.0.1; deny all;: CRITICAL SECURITY MEASURE. This restricts who can send purge requests. Only allow requests from your server itself (localhost) or specific internal IPs. Never leave this open to the world.proxy_cache_purge wp_cache "$scheme$request_method$host$1";: This is the magic. When Nginx receives a request to/nginx-cache-purge/your/page/path, it deletes the corresponding cached item from thewp_cachezone. The$1captures the(/.*)part of the regex, which is the actual URL path we want to purge.
Post-Configuration Steps
- Test Nginx configuration: After modifying
nginx.confand your site’s server block, always runsudo nginx -tto check for syntax errors. - Reload Nginx: If the test is successful, reload Nginx:
sudo service nginx reloadorsudo systemctl reload nginx. - Check cache directory permissions: Ensure that the user Nginx runs as (often
www-dataornginx) has read/write permissions to/var/cache/nginx. You might needsudo chown -R www-data:www-data /var/cache/nginxandsudo chmod -R 755 /var/cache/nginx.
If you’re looking to enhance your WordPress site’s performance, implementing full-page caching without a plugin can be a game changer, especially when using Nginx and PHP. This method not only speeds up page load times but also improves your site’s overall efficiency. For more insights on optimizing your website’s performance, you might find this article on Google PageSpeed Insights particularly helpful, as it offers valuable tips on how to analyze and improve your site’s speed.
Verifying and Debugging Your Cache
Once everything is set up, it’s essential to verify that it’s working as expected.
Checking X-Cache-Status Header
The X-Cache-Status header we added is your best friend here.
- Open your browser’s developer tools: (F12 in Chrome/Firefox).
- Go to the “Network” tab.
- Refresh your WordPress homepage (or any public page) when not logged in.
- Click on the main document request (usually your domain name).
- Look at the “Response Headers” section.
- First visit (or after cache purge): You should see
X-Cache-Status: MISS. This means Nginx didn’t find the page in its cache and passed the request to WordPress. - Subsequent visits: You should see
X-Cache-Status: HIT. This confirms the page was served directly from the Nginx cache. - Logged-in visit: You should see
X-Cache-Status: BYPASS. This indicates Nginx detected your logged-in cookies and bypassed the cache.
Observing Cache Directory
You can also check the /var/cache/nginx directory. You should see new subdirectories and files appearing as pages are cached.
sudo ls -l /var/cache/nginx/c/29/ (example based on levels=1:2)
Troubleshooting Tips
- No
HITstatus: - Are you logged in? Log out or open an incognito window.
- Check your Nginx
ifconditions with cookies. Are you sure you’re not sending any WordPress cookies that might trigger a bypass? - Is
X-Accel-Expires: 0being sent from PHP when it shouldn’t be? - Check Nginx error logs (
/var/log/nginx/yourdomain.com_error.log). - Ensure the
proxy_cache_pathis correctly defined and Nginx has permissions. - Pages not clearing:
- Is
NGINX_PURGE_PATHdefined correctly inwp-config.php? - Is the Nginx
location ~ ^/nginx-cache-purge(/.*)block correctly configured? - Is the
allow 127.0.0.1;rule too restrictive? Is thewp_remote_getrequest coming from127.0.0.1or another IP? - Check Nginx access logs for purge requests. Are they hitting the purge location?
- Check PHP error logs for
wp_remote_getfailures. - Pages showing stale content after updates: This points to the cache purge not working correctly. Re-check the points above.
- 502 Bad Gateway: This usually means PHP-FPM crashed or isn’t running. Check
sudo systemctl status php8.1-fpm(or your PHP version).
If you’re looking to enhance your WordPress site’s performance, you might find it beneficial to explore related techniques that complement full-page caching. One such approach is optimizing your server configuration for better resource management. For more insights on this topic, you can check out this informative article that dives deeper into server optimization strategies for WordPress. By implementing these strategies alongside caching, you can significantly improve your site’s speed and efficiency. To read more, visit this article.
Advanced Considerations and Fine-Tuning
Cache Exclusions
While the if conditions in our Nginx config handle common WordPress exclusions, you might need more. For example:
- E-commerce carts/checkouts: These are inherently dynamic and must never be cached. Add
if ($request_uri ~* "/(cart|checkout|my-account)") { proxy_pass ...; break; }to your Nginxlocation /block. - Search results: Often dynamic.
if ($request_uri ~* "/\?s=") { proxy_pass ...; break; } - Specific user agent patterns: If some bots or services should always hit WordPress directly.
Dynamic Content within Cached Pages
What if you have a small dynamic element (like a personalized greeting or a rotating banner) on an otherwise static page? This setup caches the entire page. To handle this, you’d typically use Edge Side Includes (ESI). Nginx supports ESI, allowing you to “punch holes” in cached pages and insert content from separate, uncached requests. This is significantly more complex and outside the scope of a basic setup, but it’s good to know it’s possible.
Cache Storage and Size
Monitor your /var/cache/nginx directory. If it fills up quickly, you can:
- Increase
max_size. - Reduce
inactivetimeout if content has a short lifespan. - Adjust
NGINX_CACHE_AGEinwp-config.phpto cache items for less time.
Security Concerns
- Purge location access: Reiterate the importance of restricting access to your purge URI. This is a potential attack vector if left open.
- Sensitive data: Always double-check that no sensitive user data is exposed on publicly cached pages. The logged-in user bypass is essential for this.
Conclusion
Implementing full-page caching in WordPress using Nginx and PHP without a dedicated plugin is a powerful way to supercharge your site’s performance. It gives you fine-grained control, reduces overhead, and leverages Nginx’s efficiency. While it requires more initial setup and a deeper understanding of your server environment, the benefits in terms of speed and stability can be significant. By carefully configuring Nginx to serve cached content and using PHP to send critical caching and purging headers, you build a lean, fast, and customizable caching layer for your WordPress site.