How to implement full-page caching in WordPress without a plugin — using Nginx and PHP?

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:

  1. Nginx checks if a cached version of the page exists.
  2. If it does, and it’s valid, Nginx serves it directly without bothering PHP.
  3. If not, Nginx passes the request to PHP.
  4. 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 named wp_cache of 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 if condition 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 check is_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: 0 to tell Nginx not to cache this response or to immediately expire any existing cache related to this key. We also add standard Cache-Control headers for browsers.
  • Anonymous users: For non-logged-in visitors, we send X-Accel-Expires: NGINX_CACHE_AGE to tell Nginx to cache this page for the defined duration.
  • The else blocks cover all other scenarios (admin, AJAX, etc.), telling Nginx not to cache them by sending X-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 an X-Accel-Expires header, Nginx will cache 200 OK responses for 30 minutes. The PHP X-Accel-Expires overrides 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 a HIT (served from cache), MISS (not in cache, served by WordPress), or BYPASS (Nginx chose not to cache).
  • Conditional proxy_pass blocks with break;: 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 the wp_cache zone. The $1 captures the (/.*) part of the regex, which is the actual URL path we want to purge.

Post-Configuration Steps

  1. Test Nginx configuration: After modifying nginx.conf and your site’s server block, always run sudo nginx -t to check for syntax errors.
  2. Reload Nginx: If the test is successful, reload Nginx: sudo service nginx reload or sudo systemctl reload nginx.
  3. Check cache directory permissions: Ensure that the user Nginx runs as (often www-data or nginx) has read/write permissions to /var/cache/nginx. You might need sudo chown -R www-data:www-data /var/cache/nginx and sudo 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.

  1. Open your browser’s developer tools: (F12 in Chrome/Firefox).
  2. Go to the “Network” tab.
  3. Refresh your WordPress homepage (or any public page) when not logged in.
  4. Click on the main document request (usually your domain name).
  5. 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 HIT status:
  • Are you logged in? Log out or open an incognito window.
  • Check your Nginx if conditions with cookies. Are you sure you’re not sending any WordPress cookies that might trigger a bypass?
  • Is X-Accel-Expires: 0 being sent from PHP when it shouldn’t be?
  • Check Nginx error logs (/var/log/nginx/yourdomain.com_error.log).
  • Ensure the proxy_cache_path is correctly defined and Nginx has permissions.
  • Pages not clearing:
  • Is NGINX_PURGE_PATH defined correctly in wp-config.php?
  • Is the Nginx location ~ ^/nginx-cache-purge(/.*) block correctly configured?
  • Is the allow 127.0.0.1; rule too restrictive? Is the wp_remote_get request coming from 127.0.0.1 or another IP?
  • Check Nginx access logs for purge requests. Are they hitting the purge location?
  • Check PHP error logs for wp_remote_get failures.
  • 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 Nginx location / 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 inactive timeout if content has a short lifespan.
  • Adjust NGINX_CACHE_AGE in wp-config.php to 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.