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:
| Value | Meaning |
|---|---|
HIT | Served from cache |
MISS | Not in cache, response was fetched from PHP |
BYPASS | Cache was bypassed (fastcgi_cache_bypass) |
EXPIRED | Entry expired, response re-fetched |
STALE | Stale entry served (with fastcgi_cache_use_stale) |
UPDATING | Stale 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.


