Nginx FastCGI Cache Configuration

Nginx FastCGI cache stores PHP-generated responses on disk, allowing Nginx to serve subsequent identical requests directly without invoking PHP-FPM. This dramatically improves throughput for PHP applications, including WordPress, by reducing backend processing for cacheable pages. This guide covers FastCGI cache zone setup, key design, purging, bypass rules, and WordPress-specific optimization.

Prerequisites

  • Nginx installed (1.18+ recommended)
  • PHP-FPM configured and running
  • A PHP application (WordPress, Laravel, etc.)
  • Root or sudo access
  • Sufficient disk space for cache storage (1-10 GB depending on site size)

FastCGI Cache Zone Configuration

Define the cache zone in the http block of nginx.conf:

# Edit /etc/nginx/nginx.conf
sudo nano /etc/nginx/nginx.conf

Add to the http block:

# /etc/nginx/nginx.conf
http {
    # ... existing configuration ...

    # Define FastCGI cache zone
    # Keys zone: name:size (10MB stores ~80,000 keys)
    # inactive: remove entries not accessed in 60 minutes
    # max_size: maximum disk space for cached responses
    fastcgi_cache_path /var/cache/nginx/fastcgi
        levels=1:2
        keys_zone=PHPCACHE:10m
        inactive=60m
        max_size=1g;

    # Default cache lock to prevent cache stampede
    fastcgi_cache_lock on;
    fastcgi_cache_lock_timeout 5s;

    # Buffer settings
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
}

Create the cache directory:

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

Enable Caching in a Server Block

# /etc/nginx/sites-available/mysite
server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.php;

    # Cache bypass map variable (set in server block or http block)
    set $skip_cache 0;

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

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

        # Enable FastCGI cache
        fastcgi_cache PHPCACHE;
        fastcgi_cache_valid 200 301 302 10m;  # Cache 200/301/302 for 10 minutes
        fastcgi_cache_valid 404 1m;            # Cache 404s for 1 minute
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        # Serve stale content while revalidating (grace period)
        fastcgi_cache_use_stale error timeout updating http_500 http_503;
        fastcgi_cache_background_update on;

        # Add cache status header
        add_header X-FastCGI-Cache $upstream_cache_status;
    }

    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Apply changes:

sudo nginx -t && sudo systemctl reload nginx

Cache Key Design

The cache key determines what constitutes a unique cacheable request. The default key is $scheme$request_method$host$request_uri:

# Custom cache key - include scheme, method, host, and URI
fastcgi_cache_key "$scheme$request_method$host$request_uri";

# For sites with query string variations:
fastcgi_cache_key "$scheme$request_method$host$uri$is_args$args";

# For sites with cookie-based personalization (advanced):
# Include a cookie value in the key for user-specific caching
fastcgi_cache_key "$scheme$request_method$host$uri$cookie_user_role";

For most PHP applications, the default or URI-based key is sufficient.

Cache Bypass Rules

Bypass the cache for requests that should never be served stale:

server {
    # ... (within server block, before location blocks)
    
    # Default: don't skip cache
    set $skip_cache 0;

    # Skip cache for POST requests
    if ($request_method = POST) {
        set $skip_cache 1;
    }

    # Skip cache for requests with query strings (optional - depends on app)
    if ($query_string != "") {
        set $skip_cache 1;
    }

    # Skip cache for logged-in WordPress users
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
        set $skip_cache 1;
    }

    # Skip cache for WooCommerce cart/checkout/account pages
    if ($request_uri ~* "/cart|/checkout|/my-account|/wc-api") {
        set $skip_cache 1;
    }

    # Skip cache when a specific query parameter is present
    if ($arg_nocache = "1") {
        set $skip_cache 1;
    }
}

Cache Status Headers

Use response headers to debug cache behavior:

# Add to the PHP location block
add_header X-FastCGI-Cache $upstream_cache_status always;

Possible values for $upstream_cache_status:

ValueMeaning
HITServed from cache
MISSNot in cache, response was fetched from PHP
BYPASSCache was bypassed (fastcgi_cache_bypass)
EXPIREDEntry expired, response re-fetched
STALEStale entry served (with fastcgi_cache_use_stale)
UPDATINGStale served while cache updated in background

Test with curl:

# First request - should be MISS
curl -I https://example.com/ | grep X-FastCGI-Cache
# X-FastCGI-Cache: MISS

# Second request - should be HIT
curl -I https://example.com/ | grep X-FastCGI-Cache
# X-FastCGI-Cache: HIT

# Check bypass rule (POST request)
curl -I -X POST https://example.com/ | grep X-FastCGI-Cache
# X-FastCGI-Cache: BYPASS

Cache Purging Strategies

Method 1: Delete cache files manually

# Clear entire cache
sudo rm -rf /var/cache/nginx/fastcgi/*

# Clear cache for a specific URL (using MD5 of the cache key)
# The key is: scheme + method + host + URI
echo -n "httpGETexample.com/" | md5sum
# Returns: abc123...

# Cache files are stored at: /var/cache/nginx/fastcgi/c/bc/abc123...
# Navigate the directory structure to find and delete specific entries
find /var/cache/nginx/fastcgi -type f -name "abc123*" -delete

Method 2: Nginx Cache Purge module

# Install nginx with purge module (Ubuntu)
sudo apt install -y libnginx-mod-http-cache-purge

# Add purge location to server block
location ~ /purge(/.*) {
    # Allow only from localhost and admin IPs
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
    
    fastcgi_cache_purge PHPCACHE "$scheme$request_method$host$1";
}

# Purge a specific URL
curl -X PURGE http://localhost/purge/path/to/page

sudo nginx -t && sudo systemctl reload nginx

Method 3: Script-based purge

cat > /usr/local/bin/nginx-cache-purge << 'SCRIPT'
#!/bin/bash
CACHE_DIR="/var/cache/nginx/fastcgi"
URL="$1"

if [ -z "$URL" ]; then
  echo "Usage: $0 <url>"
  echo "Example: $0 https://example.com/blog/post"
  exit 1
fi

# Generate cache key hash
KEY=$(echo -n "GET${URL}" | md5sum | awk '{print $1}')
SUBDIR="${KEY: -1}/${KEY: -3:2}"

find "${CACHE_DIR}/${SUBDIR}" -name "${KEY}" -delete 2>/dev/null \
  && echo "Cache purged for: ${URL}" \
  || echo "No cache entry found for: ${URL}"
SCRIPT
chmod +x /usr/local/bin/nginx-cache-purge

WordPress-Specific Configuration

Complete WordPress configuration with FastCGI cache:

# /etc/nginx/sites-available/wordpress
fastcgi_cache_path /var/cache/nginx/wp
    levels=1:2
    keys_zone=WORDPRESS:10m
    inactive=60m
    max_size=2g;

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/wordpress;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    set $skip_cache 0;

    # Bypass for logged-in users and dynamic pages
    if ($request_method = POST) { set $skip_cache 1; }
    if ($query_string != "")    { set $skip_cache 1; }

    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
        set $skip_cache 1;
    }

    if ($request_uri ~* "^/wp-admin|^/wp-login.php|/cart|/checkout|/my-account|\?add-to-cart=|\?wc-ajax=") {
        set $skip_cache 1;
    }

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

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

        fastcgi_cache WORDPRESS;
        fastcgi_cache_key "$scheme$request_method$host$request_uri";
        fastcgi_cache_valid 200 60m;
        fastcgi_cache_valid 301 302 10m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        fastcgi_cache_use_stale error timeout updating http_500 http_503;
        fastcgi_cache_background_update on;
        fastcgi_cache_lock on;

        add_header X-FastCGI-Cache $upstream_cache_status always;
    }

    # Deny access to sensitive files
    location ~ /\.(ht|git|env) { deny all; }
    location = /wp-config.php  { deny all; }
}

Troubleshooting

Cache never HIT, always MISS:

# Check if the cache directory is writable
ls -la /var/cache/nginx/fastcgi/
# Should be owned by www-data (or nginx)

# Verify cache is being created
ls -la /var/cache/nginx/fastcgi/

# Check for bypass rules being triggered
curl -v -I https://example.com/ 2>&1 | grep -i "cookie\|cache"

# Enable Nginx debug logging temporarily
# In nginx.conf: error_log /var/log/nginx/error.log debug;

Cache fills disk:

# Check current cache size
du -sh /var/cache/nginx/fastcgi/

# Reduce max_size in cache zone definition
# fastcgi_cache_path ... max_size=512m;

# Add cache cleanup via cron (nginx automatically evicts, but check)
# ls /var/cache/nginx/fastcgi | wc -l  # Count cached entries

# Reduce inactive timeout to evict more aggressively
# inactive=10m instead of 60m

PHP sessions broken with caching:

# Ensure session cookie triggers cache bypass
# Add to bypass rules:
if ($http_cookie ~* "PHPSESSID") {
    set $skip_cache 1;
}

Conclusion

Nginx FastCGI cache is one of the highest-impact optimizations for PHP applications, capable of serving thousands of requests per second for cacheable pages with no PHP-FPM overhead. Design cache keys carefully for your application's URL structure, implement bypass rules for authenticated and dynamic content, and use cache status headers to verify the cache is working effectively in production.