WordPress on a VPS: Getting Under 1 Second Load Times
My actual config for running a fast WordPress site on a $6/month VPS — Nginx, PHP-FPM, object caching, and the mistakes I made along the way.
WordPress gets a bad reputation for being slow. Most of the time, the problem isn’t WordPress itself — it’s the default stack it runs on. After a lot of trial and error, I got a basic WordPress site to consistently load under 800ms on a $6/month Hetzner VPS. Here’s exactly how.
The Stack
Before diving in, here’s what we’re building toward:
- OS: Ubuntu 22.04
- Web server: Nginx (not Apache)
- PHP: PHP 8.2 with PHP-FPM
- Database: MariaDB (faster than MySQL for WordPress workloads)
- Object cache: Redis
- Page cache: Nginx FastCGI cache
This combination eliminates the two biggest WordPress performance killers: repeated PHP execution and repeated database queries.
Why Nginx Over Apache
Apache works fine. Nginx is just better for this specific use case. It handles concurrent connections with dramatically less memory, and its FastCGI caching implementation is more predictable than Apache’s equivalent.
The numbers: Apache on the same server used ~80MB idle RAM. Nginx uses ~12MB. On a 2GB VPS, that difference matters.
Installing the Stack
# Install Nginx, PHP 8.2, MariaDB, Redis
apt install nginx php8.2-fpm php8.2-mysql php8.2-redis \
php8.2-curl php8.2-gd php8.2-mbstring php8.2-xml \
php8.2-zip mariadb-server redis-server -y
PHP-FPM Tuning
The default PHP-FPM config is too conservative. Edit /etc/php/8.2/fpm/pool.d/www.conf:
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500
Tune pm.max_children based on your RAM. Each PHP-FPM worker uses roughly 30–50MB. On a 2GB server, 20 workers is a safe ceiling.
Nginx FastCGI Cache
This is the single biggest performance win. Instead of running PHP for every request, Nginx serves cached HTML directly from disk for repeat visitors.
Cache Configuration
Create /etc/nginx/conf.d/fastcgi-cache.conf:
fastcgi_cache_path /tmp/nginx-cache levels=1:2
keys_zone=WORDPRESS:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
WordPress Site Block
server {
listen 80;
server_name yourdomain.com;
root /var/www/wordpress;
index index.php;
set $skip_cache 0;
# Skip cache for logged-in users and POST requests
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;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache $upstream_cache_status;
}
}
The X-Cache header will show HIT or MISS so you can verify caching is working.
Redis Object Cache
FastCGI cache handles full-page caching. Redis handles WordPress’s internal object cache — database query results, transients, and other computed data.
Install the plugin:
wp plugin install redis-cache --activate --path=/var/www/wordpress
Add to wp-config.php:
define('WP_CACHE_KEY_SALT', 'yourdomain.com');
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
Then enable from the plugin settings screen or via WP-CLI:
wp redis enable --path=/var/www/wordpress
The Results
After implementing all of the above on a fresh WordPress install with a lightweight theme:
| Metric | Before | After |
|---|---|---|
| Time to First Byte | 380ms | 18ms (cached) |
| Page load (no cache) | 2.1s | 820ms |
| Page load (cached) | 2.1s | 190ms |
| Memory usage | 320MB | 145MB |
The TTFB improvement from 380ms to 18ms on cached requests is the most dramatic. That’s Nginx serving static HTML from disk without touching PHP or the database at all.
Common Mistakes
A few things that cost me time when I first set this up:
Forgetting to purge cache on publish. Install the Nginx Helper plugin and point it at your cache path. Otherwise visitors see stale content after you update posts.
Running Redis without a memory limit. Add maxmemory 64mb and maxmemory-policy allkeys-lru to /etc/redis/redis.conf. Uncapped Redis will eat all your RAM eventually.
Not enabling OPcache. Add opcache.enable=1 and opcache.memory_consumption=128 to your PHP config. PHP compiles your scripts once and serves them from memory — huge win for uncached requests.
This setup handles thousands of requests per day comfortably on a single small VPS. Beyond that, you’re looking at a CDN layer — which is a whole other post.