WordPress VPS Performance Optimization: Achieve Sub-Second Load Times on Your Own Server
A freshly installed WordPress site on a 2 GB VPS might load in 3–8 seconds. The same site, properly tuned, loads in 0.3–0.8 seconds. This guide applies every relevant optimization layer: PHP-FPM tuning, Nginx FastCGI caching, MySQL buffer pool sizing, object caching with Valkey, image optimization, and Brotli compression — measured with before/after benchmarks at each step.
Baseline: Measure Before Optimizing
# Establish baseline metrics before any changes:
ab -n 200 -c 20 https://yourdomain.com/ 2>&1 | grep -E "Requests|Time per"
# Save output: "Requests per second: 45" → target: 500+
# Core Web Vitals:
# Google PageSpeed Insights: pagespeed.web.dev → enter your URL
# Save: LCP, FID/INP, CLS scores
# Server timing:
curl -w "\nDNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
-o /dev/null -s https://yourdomain.com/
Layer 1: PHP-FPM Optimization
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
# Process manager — dynamic allows scaling up/down
pm = dynamic
# Max children = (Available RAM) / (RAM per PHP-FPM process)
# Check average PHP-FPM process size: ps aux | grep php-fpm | awk '{print $6}' | sort -n | tail -5
# Typical WordPress PHP-FPM: 25–40 MB per process
# 2 GB VPS: 1500 MB available → max 40 children (40 × 35MB = 1400MB)
pm.max_children = 15 # Start conservative; increase as needed
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500 # Restart workers to prevent memory leaks
# OPcache — cache compiled PHP bytecode
# /etc/php/8.2/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 128 # MB
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 0 # Never recheck in production
opcache.validate_timestamps = 0 # Don't check file mtime (faster, clear cache on deploy)
opcache.jit = 1255 # PHP 8.x JIT compilation
opcache.jit_buffer_size = 64M
sudo systemctl restart php8.2-fpm
# Verify OPcache is working:
php -m | grep OPcache
php -r "var_dump(opcache_get_status(false)['memory_usage']);"
Layer 2: MySQL/MariaDB Optimization
sudo nano /etc/mysql/mariadb.conf.d/99-wordpress.cnf
[mysqld]
# InnoDB buffer pool — most important MySQL setting
# Set to 60–70% of RAM dedicated to MySQL
# 2 GB VPS with MySQL only: 1.2 GB
# 2 GB VPS sharing with Nginx+PHP: 256-512 MB
innodb_buffer_pool_size = 512M
# Buffer pool instances (1 per CPU core, max 8)
innodb_buffer_pool_instances = 2
# Write performance — larger reduces fsync frequency
innodb_log_file_size = 256M
innodb_flush_log_at_trx_commit = 2 # 0=fastest (no durability), 1=safest, 2=good balance
# Query cache (MariaDB only — removed in MySQL 8.0)
query_cache_type = 1
query_cache_size = 64M
query_cache_limit = 2M
# Connection optimization
max_connections = 100 # Reduce from default 151 if PHP-FPM pool is smaller
thread_cache_size = 16
# Slow query log (identify bottleneck queries)
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # Log queries slower than 1 second
sudo systemctl restart mariadb
# Verify buffer pool size:
mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"
Layer 3: Object Cache with Valkey (Redis-Compatible)
# Install Valkey (or Redis) for object caching:
docker run -d --name valkey --restart always \
-p 127.0.0.1:6379:6379 \
valkey/valkey:8-alpine \
valkey-server --requirepass YourValkeyPassword!
# Install WordPress Redis plugin:
wp plugin install redis-cache --activate
wp plugin activate redis-cache
# Configure wp-config.php:
sudo nano /var/www/wordpress/wp-config.php
# Add above "/* That's all, stop editing! */":
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_PASSWORD', 'YourValkeyPassword!');
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_REDIS_DATABASE', 0);
wp redis enable
# Verify object cache is working:
wp redis status
# Expected: Status: Connected | Used memory: X MB
Layer 4: Nginx FastCGI Cache
sudo nano /etc/nginx/nginx.conf
http {
# FastCGI cache zone
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=wordpress_cache:100m
max_size=1g
inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
}
sudo nano /etc/nginx/sites-available/wordpress
server {
# Skip cache conditions
set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($http_cookie ~* "wordpress_logged_in|comment_author|woocommerce") { set $skip_cache 1; }
if ($request_uri ~* "wp-admin|wp-login|wp-cron|xmlrpc") { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache wordpress_cache;
fastcgi_cache_valid 200 60m; # Cache successful pages for 60 min
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating;
fastcgi_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
}
}
sudo mkdir -p /var/cache/nginx/fastcgi
sudo chown www-data:www-data /var/cache/nginx/fastcgi
sudo nginx -t && sudo systemctl reload nginx
Layer 5: Image Optimization
# Install image optimization tools:
sudo apt install -y jpegoptim optipng webp
# Batch optimize existing images:
find /var/www/wordpress/wp-content/uploads -name "*.jpg" -exec jpegoptim --max=85 {} \;
find /var/www/wordpress/wp-content/uploads -name "*.png" -exec optipng -o5 {} \;
# Convert to WebP (modern browsers):
find /var/www/wordpress/wp-content/uploads -name "*.jpg" -exec sh -c \
'cwebp -q 85 "$1" -o "${1%.jpg}.webp"' _ {} \;
# WordPress plugins for automatic WebP:
# Install: ShortPixel, Imagify, or EWWW Image Optimizer
# These auto-convert on upload and serve WebP to supporting browsers
Layer 6: Brotli + Gzip Compression
# Enable in nginx.conf http block:
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/javascript
application/javascript application/json
application/xml image/svg+xml;
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/javascript
application/json image/svg+xml;
gzip_vary on;
Measure Improvement
# After all optimizations:
ab -n 1000 -c 100 https://yourdomain.com/ 2>&1 | grep -E "Requests|Time per"
# Target: "Requests per second: 500–3000" (was 45)
# Check cache hit rate:
curl -sI https://yourdomain.com/ | grep X-Cache
# First request: X-Cache-Status: MISS
# Second request: X-Cache-Status: HIT
# Check TTFB after optimization:
curl -w "TTFB: %{time_starttransfer}s\n" -o /dev/null -s https://yourdomain.com/
# Target: <0.2s (was 1–3s)
# Google PageSpeed Insights — compare scores before/after
Getting Started
Apply these optimizations in layers, benchmarking between each step. On a 2 GB Ubuntu VPS at VPS.DO with NVMe storage, a properly optimized WordPress site handles 500–3,000 requests per second for cached pages. The FastCGI cache provides the largest improvement (10–50× throughput increase for anonymous traffic). Object caching reduces database queries for logged-in users. OPcache eliminates PHP bytecode recompilation.
Conclusion
WordPress performance optimization is layered: OPcache eliminates PHP compilation overhead, PHP-FPM tuning maximizes worker efficiency, MySQL buffer pool keeps hot data in RAM, Valkey object cache eliminates redundant database queries, FastCGI cache serves anonymous traffic from memory at Nginx speed, and Brotli compression reduces transfer sizes. Applied together, these optimizations transform a slow WordPress site into one delivering sub-second page loads — without upgrading hardware.