How to Configure Nginx with FastCGI Cache

How to Configure Nginx with FastCGI Cache

How to Configure Nginx with FastCGI Cache

Somewhere around the third time I watched a WordPress site fall over during a traffic spike — despite having a perfectly reasonable server behind it — I stopped reaching for “just add more CPU” and started looking at FastCGI caching instead. The site was PHP-FPM behind Nginx, and every single request, including ones for the exact same unchanged blog post being hit thousands of times, was going all the way through PHP execution and a database query. Once I set up FastCGI cache to serve those repeated requests straight from Nginx’s memory instead, response times dropped from several hundred milliseconds to single-digit milliseconds, and PHP-FPM’s CPU usage during traffic spikes dropped dramatically. This guide covers exactly how I configure FastCGI caching, from the basics through cache invalidation and troubleshooting stale content.

What FastCGI Cache Actually Does

When Nginx proxies requests to a FastCGI backend (most commonly PHP-FPM), by default every single request re-executes the backend application — even if the exact same page was just served a second ago. FastCGI caching lets Nginx store the response from the backend and serve subsequent identical requests directly from cache, completely bypassing PHP-FPM (and whatever database queries it would have run) entirely.

This is conceptually similar to a full-page cache — for content that doesn’t change per-request (most blog posts, product pages, static-ish content), caching the whole rendered response is dramatically more efficient than regenerating it every time.

Requirements

Step 1: Confirm Your Existing PHP-FPM + Nginx Setup

Before adding caching, make sure the base setup works. A typical PHP-FPM location block looks like this:

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Confirm this works before adding caching on top — caching bugs are much harder to debug if you’re not sure whether the underlying proxy configuration is correct in the first place.

Step 2: Define the FastCGI Cache Path

Add this in the http block of nginx.conf (not inside a server block):

http {
    fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=FASTCGI_CACHE:100m inactive=60m max_size=1g;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
}

Breaking this down:

Create the cache directory with correct ownership:

sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown www-data:www-data /var/cache/nginx/fastcgi

Step 3: Enable Caching in the Server Block

server {
    listen 80;
    server_name example.com;
    root /var/www/example.com;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        fastcgi_cache FASTCGI_CACHE;
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_use_stale error timeout updating http_500 http_503;
        add_header X-FastCGI-Cache $upstream_cache_status;
    }
}

Key directives here:

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Step 4: Excluding Requests That Should Never Be Cached

This is the step people skip and then get burned by — caching logged-in user sessions, admin panels, or POST requests (like login forms or checkout flows) can leak one user’s data to another or break functionality entirely.

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    set $skip_cache 0;

    # Don't cache POST requests
    if ($request_method = POST) {
        set $skip_cache 1;
    }

    # Don't cache URLs with query strings (adjust as needed for your app)
    if ($query_string != "") {
        set $skip_cache 1;
    }

    # Don't cache admin, login, or cart pages
    if ($request_uri ~* "/wp-admin/|/login|/cart|/checkout|/my-account") {
        set $skip_cache 1;
    }

    # Don't cache for logged-in users (WordPress-style cookie check example)
    if ($http_cookie ~* "comment_author|wordpress_logged_in|wp-postpass") {
        set $skip_cache 1;
    }

    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;

    fastcgi_cache FASTCGI_CACHE;
    fastcgi_cache_valid 200 60m;
    fastcgi_cache_valid 404 1m;
    fastcgi_cache_use_stale error timeout updating http_500 http_503;
    add_header X-FastCGI-Cache $upstream_cache_status;
}

fastcgi_cache_bypass controls whether Nginx reads from cache for this request; fastcgi_no_cache controls whether it writes to cache. Setting both based on the same $skip_cache variable ensures sensitive or dynamic requests neither read stale cached data nor pollute the cache with content that shouldn’t be shared across users.

I’ve adapted the cookie-check pattern above from WordPress specifically — adjust the cookie names and excluded paths to match whatever application/framework you’re actually running.

Step 5: Testing the Cache Is Working

Request the same page twice and check the X-FastCGI-Cache header:

curl -I http://example.com/some-page/
curl -I http://example.com/some-page/

First request should show:

X-FastCGI-Cache: MISS

Second request (within the cache validity window) should show:

X-FastCGI-Cache: HIT

You can also directly inspect cache files on disk:

sudo ls -la /var/cache/nginx/fastcgi/

And measure the actual performance difference:

# First request (cache miss, hits PHP-FPM)
curl -o /dev/null -s -w "%{time_total}\n" http://example.com/some-page/

# Second request (cache hit, served by Nginx directly)
curl -o /dev/null -s -w "%{time_total}\n" http://example.com/some-page/

The difference is usually dramatic — I regularly see cached responses return in under 10ms versus 200-500ms+ for the uncached PHP-rendered version, depending on how database-heavy the page is.

Step 6: Cache Purging

Sixty-minute cache validity is fine for most content, but sometimes you need to invalidate the cache immediately — right after publishing a new blog post or updating a product price, for instance.

Manual purge (simplest approach):

sudo rm -rf /var/cache/nginx/fastcgi/*

This works but nukes the entire cache, which causes a temporary spike in backend load as everything gets regenerated at once. Fine for low-traffic sites; not ideal for busier ones.

Selective purging with ngx_cache_purge module:

For more surgical invalidation, install the third-party ngx_cache_purge module (available as a package on many distros, or compiled in if building from source):

sudo apt install libnginx-mod-http-cache-purge -y

Add a purge location:

location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    fastcgi_cache_purge FASTCGI_CACHE "$scheme$request_method$host$1";
}

Now you can purge a specific URL:

curl -X GET "http://127.0.0.1/purge/some-page/"

I integrate this into deployment scripts and CMS publish hooks so that content updates automatically purge just the affected cached pages, rather than the whole cache.

Step 7: Adding Cache Headers for Debugging and CDN Integration

If you’re also running a CDN in front of Nginx, make sure your caching headers are consistent so both layers agree on freshness:

location ~ \.php$ {
    # ... existing fastcgi config ...

    add_header X-FastCGI-Cache $upstream_cache_status;
    add_header Cache-Control "public, max-age=300" always;
}

Be careful here — Cache-Control headers affect browser and CDN caching behavior, which is a separate (though related) concern from Nginx’s own FastCGI cache. I keep browser cache TTLs shorter than the Nginx FastCGI cache TTL in most cases, so that a purge at the Nginx layer is reflected to end users reasonably quickly rather than being masked by an aggressively long browser cache.

Troubleshooting Common Issues

Cache always shows MISS, never HIT — Check your fastcgi_cache_key isn’t inadvertently unique per request (for example, if it includes a session ID or timestamp variable by mistake), and confirm $skip_cache logic isn’t unconditionally set to 1 for the page you’re testing.

Logged-in users seeing other users’ cached content — This is the most serious FastCGI cache bug and almost always traces back to missing the cookie-based $skip_cache exclusion from Step 4. Audit this carefully before going live with caching on any site with user accounts.

Cache never expires despite fastcgi_cache_valid — Double check you don’t have inactive set longer than intended, and remember inactive and fastcgi_cache_valid serve different purposes — inactive is about removing unused entries regardless of validity, while fastcgi_cache_valid is about how long content is considered fresh.

Disk filling up in the cache directory — Your max_size setting either isn’t set or is set too high for your available disk space. Also confirm cache directory permissions are correct and Nginx isn’t failing to clean up old entries due to a permissions issue.

Purge requests return 404 — Confirm the ngx_cache_purge module is actually installed and loaded (nginx -V | grep cache_purge for statically compiled versions, or check load_module for dynamic ones), and that your purge location’s regex correctly matches the URL pattern you’re trying to purge.

Security Considerations

Performance Tips

Real-World Use Cases

Best Practices I Follow

  1. Always exclude POST requests, admin paths, and authenticated-user cookies from caching before enabling it on any real site.
  2. Add the X-FastCGI-Cache debug header permanently — it costs nothing and is invaluable for ongoing troubleshooting.
  3. Use fastcgi_cache_use_stale to gracefully degrade during backend errors or slow responses rather than showing visitors an error page.
  4. Set up selective purging (via ngx_cache_purge or similar) rather than relying solely on full-cache flushes for content-driven sites.
  5. Restrict cache-purge endpoints to trusted internal sources only.
  6. Tune TTLs per content type rather than applying one blanket cache duration site-wide.
  7. Monitor cache hit ratio and disk usage as ongoing operational metrics, not just a set-and-forget configuration.
  8. Test thoroughly for cross-user data leakage before enabling caching on any site with user accounts or personalized content.

Wrapping Up

FastCGI caching is one of the highest-leverage performance changes I make on PHP-backed sites — the setup itself is a handful of directives, but the payoff (in reduced backend load, faster response times, and resilience during traffic spikes) is substantial. The part that actually requires care is correctly excluding dynamic and authenticated content from the cache; get that wrong and you risk serving one user’s data to another, which is a much worse outcome than the slow page load you were trying to fix in the first place. Start with detection via the X-FastCGI-Cache header, be conservative and thorough with your exclusion rules, and only then trust the cache to take real load off your backend.

Exit mobile version