Varnish Cache is an HTTP accelerator that sits in front of your web server and caches pages in memory. For WordPress sites, Varnish can reduce page load times from hundreds of milliseconds to under 10ms, and allow a small VPS to handle thousands of concurrent visitors. This guide covers installing, configuring, and integrating Varnish with WordPress.
Architecture Overview
The typical setup places Varnish between the client and Nginx/Apache:
Client → Varnish (:80) → Nginx (:8080) → PHP-FPM → WordPress
# Or with SSL termination:
Client → Nginx (:443 SSL) → Varnish (:6081) → Nginx (:8080) → PHP-FPM
Installation
# Ubuntu/Debian
sudo apt install varnish
# RHEL/AlmaLinux
sudo dnf install varnish
# Check version (7.x recommended)
varnishd -V
Configure Nginx Backend
Move Nginx to port 8080 so Varnish can take port 80 (or 6081 if Nginx handles SSL on 443):
# /etc/nginx/sites-available/wordpress.conf
server {
listen 8080;
server_name example.com www.example.com;
root /var/www/wordpress;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Pass real client IP to WordPress
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Varnish VCL Configuration
Create a WordPress-optimized VCL at /etc/varnish/default.vcl:
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 5s;
.first_byte_timeout = 60s;
.between_bytes_timeout = 5s;
}
# Purge ACL
acl purge {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
# Handle purge requests
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return (synth(405, "Not allowed."));
}
return (purge);
}
# Ban pattern for tag-based purging
if (req.method == "BAN") {
if (!client.ip ~ purge) {
return (synth(405, "Not allowed."));
}
ban("req.http.host == " + req.http.host + " && req.url ~ " + req.url);
return (synth(200, "Ban added."));
}
# Don't cache WordPress admin or login
if (req.url ~ "wp-(admin|login|cron)" || req.url ~ "preview=true") {
return (pass);
}
# Don't cache WooCommerce pages
if (req.url ~ "(cart|my-account|checkout|addons|wp-json)") {
return (pass);
}
# Don't cache logged-in users
if (req.http.Cookie ~ "wordpress_logged_in_|wp-postpass_|woocommerce_") {
return (pass);
}
# Strip tracking cookies that don't affect content
if (req.http.Cookie) {
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_ga|_gid|_gat|utm[a-z]+|__utm[a-z]+|fbp|_fbp)=[^;]*", "");
set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", "");
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
}
# Cache static files regardless of cookies
if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|svg|woff2|woff|ttf|eot)$") {
unset req.http.Cookie;
return (hash);
}
return (hash);
}
sub vcl_backend_response {
# Cache HTML pages for 1 hour
if (beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 1h;
set beresp.grace = 24h;
}
# Cache static assets for 30 days
if (bereq.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$") {
set beresp.ttl = 30d;
unset beresp.http.Set-Cookie;
}
# Don't cache 5xx errors
if (beresp.status >= 500) {
set beresp.ttl = 0s;
set beresp.uncacheable = true;
return (deliver);
}
# Don't cache responses with Set-Cookie
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
return (deliver);
}
return (deliver);
}
sub vcl_deliver {
# Add cache hit/miss header for debugging
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT (" + obj.hits + ")";
} else {
set resp.http.X-Cache = "MISS";
}
# Remove server identification
unset resp.http.X-Powered-By;
unset resp.http.Via;
unset resp.http.X-Varnish;
}
Varnish Service Configuration
# /etc/varnish/varnish.params or override systemd unit
# Allocate memory cache (1GB recommended for WordPress)
sudo systemctl edit varnish
[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd \
-a :6081 \
-f /etc/varnish/default.vcl \
-s malloc,1G \
-p thread_pool_min=50 \
-p thread_pool_max=1000 \
-p thread_pool_timeout=120
sudo systemctl restart varnish
WordPress Plugin Integration
Install a cache purging plugin so WordPress automatically invalidates Varnish cache when content changes:
# Recommended: Proxy Cache Purge plugin (free)
wp plugin install varnish-http-purge --activate
# Or add custom purge in your theme's functions.php
function purge_varnish_post($post_id) {
$url = get_permalink($post_id);
$parsed = parse_url($url);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "http://127.0.0.1:6081" . $parsed['path']);
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PURGE");
curl_setopt($curl, CURLOPT_HTTPHEADER, ["Host: " . $parsed['host']]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_exec($curl);
curl_close($curl);
// Also purge homepage
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "http://127.0.0.1:6081/");
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PURGE");
curl_setopt($curl, CURLOPT_HTTPHEADER, ["Host: " . $parsed['host']]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_exec($curl);
curl_close($curl);
}
add_action('save_post', 'purge_varnish_post');
SSL with Nginx Frontend
# Nginx on :443 → Varnish :6081 → Nginx :8080
# /etc/nginx/sites-available/ssl-terminator.conf
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:6081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}
Monitoring Varnish
# Real-time stats
varnishstat
# Key metrics to watch:
# MAIN.cache_hit / MAIN.cache_miss — aim for >90% hit rate
# MAIN.threads — should not hit thread_pool_max
# Live log of requests
varnishlog -q "ReqURL ~ '/'" -g request
# Top URLs by miss
varnishtop -i ReqURL -C
Grace Mode for High Availability
Grace mode serves stale content while fetching fresh content in the background, preventing thundering herd problems:
# In vcl_recv
sub vcl_recv {
# ... existing rules ...
# If backend is healthy, use normal TTL
# If unhealthy, serve stale content up to grace period
if (!std.healthy(req.backend_hint)) {
set req.grace = 24h;
} else {
set req.grace = 1h;
}
}
Summary
Varnish transforms WordPress performance by serving cached pages directly from memory at sub-millisecond latency. The key is crafting VCL rules that cache aggressively for anonymous visitors while passing requests through for logged-in users and dynamic pages. Combined with proper cache purging on content updates, a 2-core VPS with Varnish can comfortably serve the same traffic that would normally require a much larger server.