WordPress NGINX Configuration: PHP-FPM Tuning, FastCGI Cache and Redis (2026 Guide)

WordPress on NGINX is a popular combination — but a default installation leaves a lot of performance on the table. Default PHP-FPM settings are tuned for a shared hosting environment with dozens of sites; a dedicated server running one WordPress site can go much faster. Default NGINX config doesn’t enable FastCGI caching, compression, or proper static file handling. Default WordPress sends no cache headers, causing browsers to re-download the same assets on every visit.

This guide covers the complete WordPress + NGINX + PHP-FPM stack on Debian and Ubuntu: server block configuration, PHP-FPM pool tuning, FastCGI caching for anonymous traffic, object caching with Redis, security hardening, and performance verification. Uses the optimised myguard packages throughout.

NGINX Server Block for WordPress

server {
    listen 443 ssl;
    http2 on;
    server_name example.com www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    root /var/www/example.com;
    index index.php;

    # Serve static assets directly with long cache headers
    location ~* .(js|css|png|jpg|jpeg|webp|svg|woff2|woff|ttf|ico|pdf)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        log_not_found off;
        access_log off;
    }

    # WordPress permalink rewriting
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # PHP via PHP-FPM
    location ~ .php$ {
        try_files $uri =404;  # Security: don't execute non-existent PHP files
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300s;
    }

    # Block direct access to sensitive files
    location ~* /.(?:git|env|htaccess|htpasswd)$ { deny all; }
    location ~* /(wp-config.php|xmlrpc.php)$ { deny all; }
    location = /wp-config.php { deny all; }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

PHP-FPM Pool Tuning

Edit /etc/php/8.4/fpm/pool.d/www.conf. The default settings are for shared hosting. Tune for a dedicated server:

[www]
user = www-data
group = www-data

; Use Unix socket (faster than TCP for local connections)
listen = /run/php/php8.4-fpm.sock
listen.owner = www-data
listen.group = www-data

; Dynamic process management
pm = dynamic
pm.max_children = 20        ; Max concurrent PHP processes
pm.start_servers = 5        ; Start with 5 workers
pm.min_spare_servers = 3    ; Keep at least 3 idle
pm.max_spare_servers = 8    ; Keep at most 8 idle
pm.max_requests = 500       ; Recycle workers after 500 requests (prevents memory leaks)

; Slower request logging for debugging
slowlog = /var/log/php/www-slow.log
request_slowlog_timeout = 5s

; PHP settings for WordPress
php_admin_value[memory_limit] = 256M
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
php_admin_value[max_execution_time] = 120
php_admin_value[error_log] = /var/log/php/www-error.log
php_admin_flag[log_errors] = on

How to calculate pm.max_children: check your average PHP process memory usage (ps --no-headers -o rss -C php-fpm8.4 | awk '{sum+=$1} END {print sum/NR/1024 " MB"}'), then divide available RAM (minus OS and NGINX overhead) by that number. For a 2GB VPS: (2048MB – 400MB overhead) / ~80MB per process = ~20 workers.

FastCGI Cache: Serve WordPress Pages Without PHP

For unauthenticated visitors reading blog posts and pages, NGINX can cache the full PHP response and serve subsequent requests without touching PHP at all. A cached page serves in ~1ms instead of ~80ms. For a blog with most traffic being anonymous readers, this is transformative.

http {
    # Cache zone: 256MB storage
    fastcgi_cache_path /var/cache/nginx/wordpress
        levels=1:2 keys_zone=wp_cache:100m
        max_size=256m inactive=60m use_temp_path=off;

    fastcgi_cache_key "$scheme$request_method$host$request_uri";

    server {
        # Cache settings per request
        set $skip_cache 0;

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

        # Don't cache URLs with query strings (search results, paginated)
        if ($query_string != "") { set $skip_cache 1; }

        # Don't cache logged-in users or cart pages
        if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in|woocommerce_items_in_cart") {
            set $skip_cache 1;
        }

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

            fastcgi_cache        wp_cache;
            fastcgi_cache_valid 200 60m;  # Cache 200 responses for 60 minutes
            fastcgi_cache_bypass  $skip_cache;
            fastcgi_no_cache      $skip_cache;

            add_header X-FastCGI-Cache $upstream_cache_status;
        }
    }
}

Create the cache directory:

mkdir -p /var/cache/nginx/wordpress
chown www-data:www-data /var/cache/nginx/wordpress

To purge the cache when WordPress publishes or updates a post, install the Nginx Cache or Nginx Helper WordPress plugin, or use the myguard cache purge module.

Object Cache with Redis

WordPress’s default object cache is per-request — database queries are cached in memory but the cache dies at the end of each request. Redis persists the object cache between requests, dramatically reducing database load:

apt-get install redis-server php8.4-redis

# Verify Redis is running
redis-cli ping  # Should return PONG

In WordPress, install the Redis Object Cache plugin, or add to wp-config.php:

define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_CACHE', true);

With Redis object cache, a typical WordPress page that runs 30 database queries on the first load runs 2–3 on subsequent loads (just cache miss checks). For WooCommerce sites with heavy product catalog queries, the improvement is even more dramatic.

Compression for WordPress

Enable Brotli and gzip for all text content:

http {
    brotli on;
    brotli_comp_level 6;
    brotli_types text/html text/css application/javascript application/json image/svg+xml;

    gzip on;
    gzip_comp_level 6;
    gzip_vary on;
    gzip_types text/html text/css application/javascript application/json image/svg+xml;
}

Install the Brotli module: apt-get install libnginx-mod-http-brotli

Security Hardening Checklist

Beyond the server block config above:

  • PHP-Snuffleupagus: apt-get install php8.4-snuffleupagus — blocks dangerous PHP functions at the interpreter level, protects against webshells even if a plugin is compromised
  • ModSecurity WAF: apt-get install libnginx-mod-http-modsecurity — blocks SQLi, XSS, and scanner traffic before it reaches PHP
  • Rate limiting on /wp-login.php: 5 req/min per IP blocks credential stuffing
  • Block xmlrpc.php: Unless you use Jetpack or mobile app editing, add location = /xmlrpc.php { deny all; }
  • File upload validation: Snuffleupagus upload validation rejects PHP files disguised as images

Performance Verification

# Test FastCGI cache is working
curl -I https://example.com/ | grep X-FastCGI-Cache
# First request: X-FastCGI-Cache: MISS
# Second request: X-FastCGI-Cache: HIT

# Check PHP-FPM worker status
curl http://127.0.0.1/fpm-status  # Add status page in pool config

# Benchmark with ab (Apache Bench)
ab -n 1000 -c 10 https://example.com/
# Look for 'Requests per second' and 'Time per request'

# Check Brotli is serving
curl -H 'Accept-Encoding: br' -I https://example.com/ | grep Content-Encoding
# Should show: Content-Encoding: br

Frequently Asked Questions

Do I need a WordPress caching plugin if I use FastCGI cache?
For anonymous traffic, FastCGI cache at the NGINX level is more efficient than any WordPress plugin cache — it serves pages without starting PHP at all. You still need a plugin to handle cache purging (clearing the cache when you publish or update posts). WP Rocket, W3 Total Cache, or the free Nginx Helper plugin handle this.
What is the right pm.max_children value for my server?
Measure your average PHP-FPM process RSS (ps aux | grep php-fpm), then calculate: (available RAM in MB) / (average process size in MB). For a 4GB server running only WordPress, expect 30–50 workers. Leave 20–30% of RAM for NGINX, Redis, MySQL, and OS overhead.
Should I use TCP or Unix socket for PHP-FPM?
Unix socket when PHP-FPM and NGINX are on the same server — it skips the network stack entirely and is measurably faster (5–15% lower latency per PHP request). Use TCP (127.0.0.1:9000) only if NGINX and PHP-FPM are on different servers.
Does FastCGI cache work with WooCommerce?
For anonymous visitors browsing the shop: yes, very well. For logged-in users with items in cart: skip_cache logic (as shown above) ensures their cart state is always fresh. You need to also skip cache for checkout, cart, and account pages. The Nginx Helper plugin handles these exclusions automatically.
Is Angie a better choice than NGINX for WordPress?
The WordPress-specific performance and configuration is identical. Angie’s advantages — native ACME (no Certbot), JSON monitoring API — benefit server management, not WordPress performance directly. If you want Let’s Encrypt without Certbot complexity, Angie is worth the switch. WordPress won’t care either way.

Related Posts