Docs / Performance Optimization / Varnish Page Caching for WordPress

Varnish Page Caching for WordPress

By Admin · Mar 15, 2026 · Updated Apr 23, 2026 · 273 views · 5 min read

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.

Was this article helpful?