How to Configure Nginx FastCGI Caching on RHEL 7
FastCGI caching in Nginx allows you to store responses from your PHP backend — typically PHP-FPM — directly on disk or in memory, serving subsequent identical requests without re-executing PHP. On a busy RHEL 7 server this can reduce backend load by an order of magnitude, cutting response times from hundreds of milliseconds to single-digit milliseconds for cached pages. This guide walks you through every layer of Nginx FastCGI caching: declaring the cache zone, keying and validating entries, wiring it into your server block, bypassing the cache for authenticated users, adding diagnostic headers, and purging stale content.
Prerequisites
- RHEL 7 with a valid subscription or access to EPEL/Remi repositories
- Nginx 1.10 or later installed and running (
systemctl status nginx) - PHP-FPM configured and listening on a Unix socket or TCP port
- Root or sudo access
- SELinux awareness — file-context labelling for the cache directory may be required
Step 1: Understand How Nginx FastCGI Caching Works
Nginx FastCGI caching intercepts the response that PHP-FPM (or any FastCGI process) returns and writes it to a cache zone. The zone is a combination of shared memory (for the key lookup index) and a directory on disk (for the cached response bodies). When the same request arrives again, Nginx checks the index, finds a valid entry, and serves the file directly — PHP is never called.
Three directives form the foundation:
- fastcgi_cache_path — defines the disk location, memory zone size, and expiry rules for inactive entries
- fastcgi_cache_key — a string Nginx hashes to produce a unique cache key per request
- fastcgi_cache_valid — how long a cached response is considered fresh for a given HTTP status code
Step 2: Create the Cache Directory
Choose a path with enough disk space. A dedicated tmpfs mount gives the best performance for high-traffic sites.
# Create the directory
sudo mkdir -p /var/cache/nginx/fastcgi
# Give Nginx ownership
sudo chown nginx:nginx /var/cache/nginx/fastcgi
# Apply the correct SELinux file context so Nginx can write to it
sudo semanage fcontext -a -t httpd_cache_t "/var/cache/nginx/fastcgi(/.*)?"
sudo restorecon -Rv /var/cache/nginx/fastcgi
If semanage is not installed, add it with sudo yum install -y policycoreutils-python.
Step 3: Declare the Cache Zone with fastcgi_cache_path
The fastcgi_cache_path directive belongs in the http block of your Nginx configuration — typically /etc/nginx/nginx.conf. Open that file and add the following inside the http { } block, outside any server { } block:
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=PHP_CACHE:100m
max_size=2g
inactive=60m
use_temp_path=off;
Parameter breakdown:
- levels=1:2 — creates a two-level directory hierarchy to avoid thousands of files in one folder
- keys_zone=PHP_CACHE:100m — names the zone
PHP_CACHEand allocates 100 MB of shared memory for the key index (roughly 800,000 keys per 1 MB) - max_size=2g — caps total disk usage at 2 GB; Nginx evicts least-recently-used entries when this is reached
- inactive=60m — entries not accessed within 60 minutes are deleted regardless of validity
- use_temp_path=off — writes directly to the cache directory, avoiding an extra file copy
Step 4: Set the Cache Key
The cache key determines what makes two requests “the same.” A minimal but practical key includes the scheme, host, and full URI including the query string:
fastcgi_cache_key "$scheme$request_method$host$request_uri";
Place this also in the http block so it applies globally, or inside a specific server block to scope it. If your application varies output by cookie (e.g., a locale cookie), append $cookie_lang to the key. Never include session cookies in the key — that defeats the purpose of caching and creates one cache entry per user.
Step 5: Enable Caching in the Location Block
Within your server block, find (or create) the location ~ .php$ block that proxies requests to PHP-FPM and add the cache directives:
server {
listen 80;
server_name example.com www.example.com;
root /var/www/html;
index index.php index.html;
# Map to control cache bypass
set $skip_cache 0;
# Do not cache POST requests
if ($request_method = POST) {
set $skip_cache 1;
}
# Do not cache URIs containing query strings
if ($query_string != "") {
set $skip_cache 1;
}
# Do not cache certain paths
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap") {
set $skip_cache 1;
}
# Do not cache for logged-in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}
location ~ .php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Cache settings
fastcgi_cache PHP_CACHE;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
# Add a header so you can see cache hits/misses
add_header X-Cache-Status $upstream_cache_status;
}
}
Step 6: Configure fastcgi_cache_valid for Multiple Status Codes
You can specify different TTLs for different HTTP status codes, which is important for not caching errors for too long:
fastcgi_cache_valid 200 301 302 60m; # Cache successful and redirect responses for 1 hour
fastcgi_cache_valid 404 1m; # Cache not-found responses for only 1 minute
fastcgi_cache_valid any 10m; # Cache everything else for 10 minutes
Step 7: Add the X-Cache-Status Header for Diagnostics
The $upstream_cache_status variable contains one of the following values: HIT, MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED. Adding it as a response header lets you verify caching behaviour with curl:
add_header X-Cache-Status $upstream_cache_status always;
Test it after reloading Nginx:
curl -sI http://example.com/ | grep X-Cache-Status
# First request → X-Cache-Status: MISS
# Second request → X-Cache-Status: HIT
Step 8: Bypass the Cache for Logged-In Users
The $skip_cache variable already handles WordPress session cookies in Step 5. The key directives are:
- fastcgi_cache_bypass $skip_cache — when
$skip_cacheis 1, Nginx fetches from PHP-FPM even if a cached copy exists - fastcgi_no_cache $skip_cache — when
$skip_cacheis 1, the fresh response from PHP-FPM is not stored in the cache
Using both directives together ensures that logged-in users always receive live PHP output and that their personalised responses do not overwrite cached public pages.
Step 9: Cache Purging Approaches
Nginx open-source does not include a built-in purge module. Two practical approaches exist:
Option A: Manual cache wipe
# Remove all cached files (safe to do while Nginx is running)
sudo find /var/cache/nginx/fastcgi -type f -delete
Option B: ngx_cache_purge module (Nginx Plus or self-compiled)
If you compile Nginx with the ngx_cache_purge third-party module, you can expose a purge endpoint:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
fastcgi_cache_purge PHP_CACHE "$scheme$request_method$host$1";
}
Then purge a specific URL with:
curl -X PURGE http://127.0.0.1/purge/path/to/page
Option C: Stale-while-revalidate pattern
Add these directives so Nginx serves stale content while it refreshes in the background, eliminating cache-miss latency spikes:
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_lock on;
fastcgi_cache_lock_timeout 5s;
Step 10: Reload Nginx and Verify
# Test configuration syntax
sudo nginx -t
# Reload without dropping connections
sudo systemctl reload nginx
# Watch the cache directory populate
watch -n 1 'du -sh /var/cache/nginx/fastcgi'
# Check for cache hits
curl -sI http://example.com/ | grep -i cache
If nginx -t reports errors, check for misplaced directives — fastcgi_cache_path and fastcgi_cache_key must be in the http block, while fastcgi_cache, fastcgi_cache_valid, fastcgi_cache_bypass, and fastcgi_no_cache belong in a location block.
Conclusion
Nginx FastCGI caching is one of the most impactful performance improvements you can make to a PHP-backed website on RHEL 7 without changing your application code. By carefully tuning the cache key to include only the dimensions that actually differentiate responses, setting appropriate TTLs per status code, and using the bypass/no-cache flags for authenticated sessions, you gain a full-page cache that is both fast and correct. Monitor X-Cache-Status headers and the cache directory size regularly as traffic patterns evolve, and revisit your inactive and max_size parameters accordingly.