Skip to main content
WP HealthKit

WordPress Varnish Cache: VCL Customization Deep Dive

August 6, 202615 min readPerformanceBy Jamie

Table of Contents

Introduction

WordPress Varnish cache VCL customization enables HTTP caching at a layer between your origin WordPress server and end users. Varnish Cache is a reverse proxy caching system that stores full HTML pages, dramatically reducing origin server load. However, default Varnish configurations don't understand WordPress's complexity. Custom VCL (Varnish Configuration Language) tells Varnish exactly how to cache WordPress pages, when to bypass caching, and how to invalidate stale content.

This deep dive explores advanced VCL customization specific to WordPress environments. You'll learn how to cache authenticated user pages separately, purge cache on content updates, handle complex cookie scenarios, and use Edge Side Includes for dynamic content blocks.

Varnish operates as a transparent proxy between clients and your WordPress origin. When a user requests a page, Varnish checks if it has a cached copy. If found, Varnish serves the cached page instantly—no origin server processing required. If not cached, Varnish fetches from the origin, caches the response, and serves it.

The performance impact is dramatic. Origin servers handling 500 requests per second can reduce to 50 requests per second with Varnish caching. Page load times drop from seconds to milliseconds. Bandwidth usage decreases by 80-90%.

However, WordPress presents caching challenges. User-specific content (logged-in user pages, personalized sidebars) must not be cached universally. Pages must invalidate when content updates. Comments should reflect immediately without requiring cache invalidation.

WP HealthKit analyzes your Varnish configuration and VCL customization to identify caching gaps and missed optimization opportunities. Many WordPress sites run Varnish with default settings that leave 50%+ of cacheable content uncached due to conservative cache rules.

Varnish Cache Fundamentals for WordPress

Varnish processes requests in several stages:

  1. recv: Initial request processing—decide whether to fetch from cache
  2. vcl_hash: Determine cache key (normally URL, but can include headers)
  3. backend_fetch: Fetch from origin if not cached
  4. deliver: Send cached or fetched response to client
  5. vcl_purge: Handle cache invalidation requests

Default Varnish behavior assumes all requests are cacheable unless headers indicate otherwise. WordPress changes this assumption—most WordPress requests should only cache for anonymous users.

Cache Headers in WordPress

WordPress origin servers should emit cache control headers:

Cache-Control: public, max-age=3600

For authenticated users:

Cache-Control: private, max-age=0, no-cache

Varnish respects these headers. When WordPress emits Cache-Control: private, Varnish bypasses caching for that request. However, many WordPress servers don't emit appropriate headers. Custom VCL compensates by analyzing request context.

VCL Request Flow

VCL code runs at each request stage, modifying behavior:

sub vcl_recv {
    # Decide if we should fetch from cache or bypass
}

sub vcl_hash {
    # Create cache key
}

sub vcl_backend_response {
    # Process origin response before caching
}

sub vcl_deliver {
    # Prepare response to client
}

VCL Customization Essentials

Basic VCL Structure for WordPress

A minimal WordPress VCL starts with this pattern:

vcl 4.1;

import std;

backend default {
    .host = "wordpress-origin.internal";
    .port = "80";
    .connect_timeout = 5s;
    .first_byte_timeout = 15s;
    .between_bytes_timeout = 5s;
}

sub vcl_recv {
    # Remove port from Host header to improve cache hit ratio
    if (req.http.Host ~ ":") {
        set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
    }
    
    # Don't cache non-GET requests
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }
    
    # Bypass cache for admin and login pages
    if (req.url ~ "^/wp-admin/|^/wp-login.php") {
        return (pass);
    }
    
    # Bypass cache for authenticated users
    if (req.http.Cookie ~ "wordpress_logged_in" || 
        req.http.Cookie ~ "comment_author") {
        return (pass);
    }
}

sub vcl_hash {
    hash_data(req.url);
    
    # Include Host in cache key for multisite
    hash_data(req.http.Host);
    
    return (lookup);
}

sub vcl_backend_response {
    # Cache successful responses for 1 hour
    if (beresp.status == 200) {
        set beresp.ttl = 1h;
        set beresp.grace = 24h;
    }
}

sub vcl_deliver {
    # Add debug header showing cache status
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT (" + obj.hits + ")";
    } else {
        set resp.http.X-Cache = "MISS";
    }
    
    set resp.http.X-Cache-Varnish = "true";
}

This basic VCL:

  • Normalizes Host header (improves hit ratio by treating example.com and example.com:80 as identical)
  • Only caches GET and HEAD requests
  • Bypasses caching for WordPress admin and login pages
  • Bypasses caching for authenticated users
  • Caches responses for 1 hour
  • Adds debug headers

VCL Variable Reference

Key Varnish variables:

  • req.url: Request URL
  • req.method: HTTP method (GET, POST, etc.)
  • req.http.Cookie: Request cookies
  • req.http.User-Agent: User agent string
  • beresp.status: Backend response status code
  • beresp.ttl: Time to live for cached object
  • obj.hits: Cache hit count

Cache Purge on Post Updates

Cached pages become stale when WordPress content updates. Effective cache purging invalidates related pages immediately.

Purge Handler in VCL

Add purge handling to VCL:

acl purge {
    "localhost";
    "127.0.0.1";
    "wordpress-origin.internal";
}

sub vcl_recv {
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return (synth(403, "Purge denied"));
        }
        return (purge);
    }
}

sub vcl_purge {
    return (synth(200, "Purge successful"));
}

This allows requests from trusted IPs to purge the cache.

WordPress Hook for Cache Purging

WordPress can trigger cache purges when content updates:

<?php
// Purge Varnish cache when posts update
function purge_varnish_cache($post_id) {
    $post = get_post($post_id);
    
    if (empty($post) || $post->post_status !== 'publish') {
        return;
    }
    
    $varnish_url = 'http://varnish-cache.internal';
    
    // Purge the post page
    $post_url = get_permalink($post_id);
    wp_remote_request($post_url, [
        'method' => 'PURGE',
        'sslverify' => false,
    ]);
    
    // Purge the home page
    wp_remote_request(home_url('/'), [
        'method' => 'PURGE',
        'sslverify' => false,
    ]);
    
    // Purge category archives
    $terms = wp_get_post_terms($post_id, 'category');
    foreach ($terms as $term) {
        $term_url = get_term_link($term);
        wp_remote_request($term_url, [
            'method' => 'PURGE',
            'sslverify' => false,
        ]);
    }
}

add_action('transition_post_status', function($new, $old, $post) {
    if ($old !== 'publish' && $new === 'publish') {
        purge_varnish_cache($post->ID);
    }
}, 10, 3);

This WordPress code automatically purges related pages when posts publish.

Smart Cache Invalidation

Selective purging is more efficient than invalidating entire cache:

<?php
// Selective purge with URL patterns
function purge_varnish_pattern($pattern) {
    $varnish_url = 'http://varnish-cache.internal';
    
    // Use regex pattern in PURGE request
    wp_remote_request($varnish_url, [
        'method' => 'PURGE',
        'headers' => [
            'X-Purge-Pattern' => $pattern,
        ],
    ]);
}

// Purge all product pages starting with /product/
purge_varnish_pattern('^/product/.*');

WordPress uses cookies for authentication and tracking. Varnish must handle cookies carefully to avoid serving cached authenticated pages to unauthenticated users.

sub vcl_recv {
    # Identify WordPress session cookies
    set req.http.WP-Auth = "false";
    
    if (req.http.Cookie ~ "wordpress_logged_in") {
        set req.http.WP-Auth = "true";
    }
    
    if (req.http.Cookie ~ "wordpress_sec") {
        set req.http.WP-Auth = "true";
    }
    
    # Bypass cache for authenticated users
    if (req.http.WP-Auth == "true") {
        return (pass);
    }
    
    # Remove unnecessary cookies that hurt cache hit ratio
    set req.http.Cookie = regsuball(req.http.Cookie, 
        "ga=([^;]+)(;[ ]*)?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, 
        "__utma=([^;]+)(;[ ]*)?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, 
        "__utmz=([^;]+)(;[ ]*)?", "");
    
    # Clean up cookie header
    set req.http.Cookie = regsuball(req.http.Cookie, 
        ";[ ]*$", "");
}

This approach:

  • Detects WordPress authentication cookies
  • Bypasses cache for authenticated users
  • Removes analytics cookies that don't affect cache validity
  • Prevents cache fragmentation from analytics cookies

Some WordPress plugins use sessions. Handle session cookies without caching:

sub vcl_hash {
    # If user has session, don't use cached version
    if (req.http.Cookie ~ "PHPSESSID|wordpress_session") {
        set req.http.Cache-Bypass = "session";
        return (lookup);
    }
    
    hash_data(req.url);
    hash_data(req.http.Host);
    return (lookup);
}

Edge Side Includes for Dynamic Content

Some WordPress content is dynamic (user-specific sidebars, comments) but other parts are static (post content). Edge Side Includes (ESI) enable caching static parts while updating dynamic parts.

ESI in VCL

sub vcl_backend_response {
    # Enable ESI processing for certain content types
    if (beresp.http.Content-Type ~ "text/html") {
        set beresp.do_esi = true;
    }
}

WordPress with ESI

WordPress can emit ESI includes:

<?php
// Render static post content in Varnish cache
// but fetch sidebar dynamically
echo '<article>';
echo get_the_content(); // Cached part
echo '</article>';

// Sidebar fetched fresh each request
echo '<!--esi<aside>';
echo dynamic_sidebar('primary');
echo '</aside>esi-->';

Varnish caches the full page but processes the ESI include—fetching sidebar content fresh on each request. This provides caching benefits without stale personalized content.

Advanced VCL Patterns

Tiered Cache with Grace

Grace allows serving stale content while fetching fresh versions:

sub vcl_backend_response {
    set beresp.ttl = 10m;
    set beresp.grace = 6h;
}

sub vcl_hit {
    # Check if cached object is stale
    if (obj.ttl <= 0s) {
        # Try to fetch fresh version asynchronously
        if (std.healthy(req.backend_hint)) {
            return (deliver);
        }
    }
}

Users get instant stale responses while Varnish fetches fresh content in the background.

Conditional Caching by Device

Cache different versions for mobile vs desktop:

sub vcl_recv {
    # Detect mobile devices
    if (req.http.User-Agent ~ "mobile|android|iphone") {
        set req.http.X-Device = "mobile";
    } else {
        set req.http.X-Device = "desktop";
    }
}

sub vcl_hash {
    hash_data(req.url);
    hash_data(req.http.Host);
    hash_data(req.http.X-Device);
    return (lookup);
}

Varnish maintains separate caches for mobile and desktop requests.

FAQ

How much traffic can Varnish handle compared to origin WordPress?

Varnish can handle 100-1000x more traffic than origin WordPress for cached content. A single Varnish instance can serve terabytes of data per month from a modest origin server. Cache hit ratios typically reach 80-95% for WordPress sites with proper VCL configuration.

What's the difference between purge and ban in Varnish?

Purge removes specific objects immediately. Ban marks objects as banned—they're removed when accessed or on next scan. For WordPress, purge is preferred for specific pages (single post, home page). Ban suits pattern-based invalidation (all product pages).

Can Varnish cache authenticated user pages?

Yes, with VCL modifications. Cache pages per-user by including user ID in cache key. This requires sophisticated VCL and careful cookie handling. For most WordPress sites, the complexity outweighs benefits—simpler to bypass authenticated traffic entirely.

How do I handle WordPress comments with Varnish caching?

Cache post pages but bypass cache for comment form submissions. When comments are approved, purge the post page cache. This ensures comments appear quickly without full cache invalidation. WP HealthKit identifies comment handling issues in your Varnish setup.

Does Varnish work with WordPress multisite?

Yes. Include Host header in cache key to separate sites. Each WordPress multisite network gets independent caches in Varnish. Be careful with cookies—ensure they're site-specific.

What VCL version should I use for WordPress?

Use VCL 4.1 or newer. VCL 4.0 is the minimum for modern WordPress. WP HealthKit can audit your VCL version and suggest upgrades.

Broader Context and Best Practices

Performance optimization in WordPress plugins requires understanding the full request lifecycle, from the initial HTTP request through PHP execution, database queries, and response generation. Every millisecond added to this cycle multiplies across every page load for every visitor. A plugin that adds just 50 milliseconds of overhead might seem insignificant, but on a site serving 100,000 page views per day, that translates to nearly 1,400 hours of cumulative user waiting time per year.

Database queries are the most common performance bottleneck in WordPress plugins, but not all query optimization strategies are equally effective. Adding an index speeds up read operations but slows down writes. Caching eliminates queries entirely but introduces cache invalidation complexity. Understanding these trade-offs is essential for making informed optimization decisions.

Core Web Vitals have fundamentally changed how performance is measured and valued. Google's inclusion of LCP, FID, and CLS as ranking factors means that plugin performance now directly impacts site owners' search visibility and revenue. Plugin developers who ignore performance are not just creating a poor user experience — they are actively harming their users' business outcomes.

The relationship between performance and security is often overlooked but critically important. Performance bottlenecks can become denial-of-service vectors when attackers identify expensive operations they can trigger repeatedly. A poorly optimized database query that takes two seconds under normal load might be weaponized to consume all available database connections.

Broader Industry Context and Best Practices

WordPress performance optimization requires understanding the full request lifecycle, from DNS resolution through server-side processing to client-side rendering. Each stage presents optimization opportunities: DNS prefetching reduces lookup latency, server-side caching eliminates redundant database queries, and client-side optimization reduces time to interactive. WP HealthKit identifies performance bottlenecks across the entire stack, providing actionable recommendations that prioritize improvements by expected impact. Performance monitoring should establish baselines and track trends over time, enabling teams to detect gradual degradation before it becomes noticeable to users or affects search engine rankings.

Database performance often represents the most significant optimization opportunity for WordPress sites. As content grows, unoptimized queries that performed adequately with small datasets can become prohibitively slow. Index analysis, query optimization, and strategic denormalization can dramatically improve response times. WP HealthKit scans for common database performance anti-patterns including missing indexes, N+1 query problems, and unnecessary autoloaded options. Regular database maintenance operations like optimizing tables, cleaning expired transients, and archiving old revisions help maintain performance over time. Query logging during development catches problematic patterns before they reach production environments.

Caching architecture for WordPress sites should implement multiple layers, each addressing different performance characteristics. Object caching with Redis or Memcached reduces database load for frequently accessed data. Page caching with Varnish or Nginx FastCGI cache eliminates PHP processing entirely for anonymous visitors. CDN caching distributes static assets globally, reducing latency for geographically distributed audiences. WP HealthKit evaluates caching effectiveness and identifies opportunities for improvement, helping teams implement the right caching strategy for their traffic patterns. Cache invalidation strategies must balance freshness requirements against performance gains, ensuring users see current content without sacrificing the speed benefits of caching.

Maintaining WordPress security and code quality at scale requires systematic approaches that go beyond individual plugin audits. Organizations managing portfolios of WordPress sites benefit from standardized assessment criteria, automated scanning schedules, and centralized reporting dashboards that aggregate findings across all properties. This systematic approach enables pattern recognition, where recurring issues across multiple sites indicate systemic problems that warrant architectural solutions rather than individual fixes. WP HealthKit provides the foundation for this systematic approach, offering consistent automated assessment that scales from single sites to enterprise portfolios without proportional increases in manual effort or specialized security staffing.

Frequently Asked Questions

How does WP HealthKit identify performance issues in plugins?

WP HealthKit analyzes database query patterns, memory usage, asset loading strategies, and caching implementation to identify performance bottlenecks. The tool flags unoptimized queries, excessive autoloaded options, missing indexes, and inefficient hook usage that impact page load times.

What are the most common WordPress plugin performance problems?

The most frequent performance issues include unindexed database queries on large tables, excessive autoloaded options consuming memory on every page load, redundant HTTP requests to external APIs, unoptimized asset loading without conditional enqueuing, and missing object caching for expensive computations.

How do Core Web Vitals relate to WordPress plugin performance?

Plugins directly impact Core Web Vitals through their effect on server response time affecting Largest Contentful Paint, JavaScript execution blocking First Input Delay, and layout-shifting content affecting Cumulative Layout Shift. Poor plugin performance can significantly harm a site's search rankings.

Should I use Redis or Memcached for WordPress object caching?

Redis offers persistence, data structures, and replication features that make it more versatile for WordPress. Memcached provides simpler multi-threaded performance for pure key-value caching. For most WordPress installations, Redis is the better choice due to its broader feature set and active community support.

How can I measure the performance impact of my WordPress plugin?

Use Query Monitor to track database queries and hooks, profile with Xdebug or Blackfire to identify bottlenecks, run load tests with k6 or Apache Bench to measure throughput, and monitor Core Web Vitals with Lighthouse CI to ensure your plugin meets performance standards.

Conclusion

WordPress Varnish cache VCL customization transforms caching from a generic reverse proxy into a WordPress-aware acceleration layer. Proper VCL handles authentication, purges stale content, manages cookies, and enables dynamic content through ESI.

Start with basic VCL patterns (bypass auth, cache GET requests). Progress to smart purging tied to WordPress update hooks. Implement ESI for complex dynamic content. Monitor cache hit ratios to identify optimization opportunities.

WP HealthKit audits your Varnish configuration and provides personalized VCL recommendations. Upload your WordPress site to WP HealthKit to get specific suggestions for improving your Varnish cache hit ratio and performance.

Internal Links:

External Resources:

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 62 verification layers.

Comments

WordPress Varnish Cache: VCL Customization Deep Dive | WP HealthKit