Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Reverse Proxy and Caching with Varnish

By Kokil Thapa | Last reviewed: August 2026

You’ve built a Laravel, WordPress, or Symfony app that works—but under traffic, response times creep into the hundreds of milliseconds. Database queries, PHP execution, and asset compilation add up. A reverse proxy and caching layer like Varnish sits in front of your application, intercepts requests, and serves cached responses in microseconds instead of milliseconds. On a real client project for a high-traffic WooCommerce store in Nepal, adding Varnish cut average response time from 450 ms to 35 ms and reduced server CPU usage by 70 %. If you’re running a production web system in 2026, reverse proxy and caching with Varnish is one of the fastest wins you can deploy.

What is a reverse proxy and how does Varnish fit?

A reverse proxy sits between clients and your web server. Instead of clients hitting Apache or Nginx directly, they hit the proxy, which forwards requests to the backend. Varnish adds HTTP caching: it stores responses and serves them directly on subsequent requests, eliminating PHP execution and database queries. The result is sub-10 ms response times for cacheable content.

Varnish is not a web server—it’s a dedicated HTTP accelerator. It speaks HTTP/1.1 and HTTP/2, supports ESI (Edge Side Includes), and is designed for zero-copy memory caching. Unlike Nginx’s proxy cache, Varnish gives you a full programming language (VCL) to control caching logic, making it ideal for complex applications like eCommerce or legal-tech portals.

ClientVarnishBackendDatabaseCached responseMiss → backend
Reverse proxy and caching with Varnish: client requests hit Varnish first. Cache hits return instantly; misses forward to the backend.

How do you install and configure Varnish as a reverse proxy?

Varnish 7.x is the current stable series in 2026. Install it on Ubuntu 24.04 LTS (or Debian 12) with these commands:

sudo apt update
sudo apt install -y varnish
sudo systemctl enable varnish

Varnish listens on port 6081 by default. To use it as a reverse proxy on port 80, edit the systemd service file:

sudo systemctl edit --full varnish

Change the ExecStart line to:

ExecStart=/usr/sbin/varnishd \
    -a :80 \
    -f /etc/varnish/default.vcl \
    -s malloc,256m

Reload systemd and restart Varnish:

sudo systemctl daemon-reload
sudo systemctl restart varnish

Now move your backend (Apache or Nginx) to port 8080. For Apache:

sudo sed -i 's/Listen 80/Listen 8080/' /etc/apache2/ports.conf
sudo systemctl restart apache2

For Nginx:

sudo sed -i 's/listen 80/listen 8080/' /etc/nginx/sites-enabled/*
sudo systemctl restart nginx

Verify Varnish is listening on port 80:

curl -I http://localhost

You should see Via: 1.1 varnish (Varnish/7.5) in the response headers.

How do you write VCL rules for Laravel, WordPress, and Symfony?

Varnish uses VCL (Varnish Configuration Language) to define caching rules. The default VCL (/etc/varnish/default.vcl) is a starting point, but you need to customise it for your framework.

Laravel VCL

Laravel apps typically need to bypass cache for authenticated users, POST requests, and certain routes. Here’s a minimal VCL for Laravel 12:

vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Bypass cache for POST, PUT, DELETE
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Bypass cache for authenticated users
    if (req.http.Authorization || req.http.Cookie ~ "laravel_session") {
        return (pass);
    }

    # Bypass cache for API routes
    if (req.url ~ "^/api/") {
        return (pass);
    }

    # Bypass cache for admin routes
    if (req.url ~ "^/admin/") {
        return (pass);
    }

    # Remove query strings for static assets
    if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|woff2?|ttf|eot|svg)(\?.*)?$") {
        set req.url = regsub(req.url, "\?.*$", "");
    }

    return (hash);
}

sub vcl_backend_response {
    # Cache for 5 minutes by default
    set beresp.ttl = 5m;

    # Do not cache responses with Set-Cookie
    if (beresp.http.Set-Cookie) {
        set beresp.ttl = 0s;
    }

    # Cache 404s for 1 minute
    if (beresp.status == 404) {
        set beresp.ttl = 1m;
    }

    return (deliver);
}

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

WordPress VCL

WordPress needs special handling for logged-in users, admin, and preview URLs. Here’s a VCL that works with WordPress 6.7+:

vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Bypass cache for POST, PUT, DELETE
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Bypass cache for logged-in users
    if (req.http.Cookie ~ "wordpress_logged_in_|wp-postpass_|comment_author_") {
        return (pass);
    }

    # Bypass cache for admin, preview, and feed
    if (req.url ~ "^/wp-(admin|login|register|comments/feed|json)") {
        return (pass);
    }

    # Bypass cache for WooCommerce pages
    if (req.url ~ "^/(cart|checkout|my-account|addons|product/.*/add-to-cart)") {
        return (pass);
    }

    # Remove query strings for static assets
    if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|woff2?|ttf|eot|svg)(\?.*)?$") {
        set req.url = regsub(req.url, "\?.*$", "");
    }

    return (hash);
}

sub vcl_backend_response {
    # Cache for 10 minutes by default
    set beresp.ttl = 10m;

    # Do not cache responses with Set-Cookie
    if (beresp.http.Set-Cookie) {
        set beresp.ttl = 0s;
    }

    # Cache 404s for 1 minute
    if (beresp.status == 404) {
        set beresp.ttl = 1m;
    }

    return (deliver);
}

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

Symfony VCL

Symfony 7 apps typically use session cookies and CSRF tokens. Here’s a VCL that respects Symfony’s defaults:

vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Bypass cache for POST, PUT, DELETE
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # Bypass cache for authenticated users
    if (req.http.Cookie ~ "_session|csrf_token") {
        return (pass);
    }

    # Bypass cache for admin routes
    if (req.url ~ "^/admin/") {
        return (pass);
    }

    # Remove query strings for static assets
    if (req.url ~ "\.(css|js|jpg|jpeg|png|gif|ico|woff2?|ttf|eot|svg)(\?.*)?$") {
        set req.url = regsub(req.url, "\?.*$", "");
    }

    return (hash);
}

sub vcl_backend_response {
    # Cache for 5 minutes by default
    set beresp.ttl = 5m;

    # Do not cache responses with Set-Cookie
    if (beresp.http.Set-Cookie) {
        set beresp.ttl = 0s;
    }

    # Cache 404s for 1 minute
    if (beresp.status == 404) {
        set beresp.ttl = 1m;
    }

    return (deliver);
}

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

After editing /etc/varnish/default.vcl, reload Varnish:

sudo systemctl reload varnish

How do you handle cache invalidation and purge?

Varnish supports cache invalidation via HTTP PURGE requests. You need to:

  1. Allow PURGE requests in VCL.
  2. Configure your application to send PURGE requests when content changes.

VCL for PURGE support

Add this to your vcl_recv subroutine:

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return (synth(405, "Method not allowed"));
    }
    return (purge);
}

And define the purge ACL at the top of your VCL:

acl purge {
    "127.0.0.1";
    "localhost";
    "::1";
}

Laravel PURGE integration

Use the spatie/laravel-varnish package (compatible with Laravel 12):

composer require spatie/laravel-varnish

Publish the config:

php artisan vendor:publish --provider="Spatie\Varnish\VarnishServiceProvider"

Configure the Varnish host in .env:

VARNISH_HOST=127.0.0.1
VARNISH_PORT=80

Now you can purge URLs from your application:

use Spatie\Varnish\Facades\Varnish;

// Purge a single URL
Varnish::purge('https://example.com/blog');

// Purge all URLs matching a pattern
Varnish::purgeAll('https://example.com/products/*');

WordPress PURGE integration

Use the WordPress Varnish HTTP Purge plugin (compatible with WordPress 6.7+):

wp plugin install varnish-http-purge --activate

Configure the Varnish host in the plugin settings (Settings → Varnish HTTP Purge).

Symfony PURGE integration

Use the symfony/http-client component to send PURGE requests:

use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create();
$response = $client->request('PURGE', 'http://127.0.0.1/blog', [
    'headers' => [
        'Host' => 'example.com',
    ],
]);

if ($response->getStatusCode() !== 200) {
    // Handle error
}

How do you monitor and debug Varnish?

Varnish provides several tools for monitoring and debugging:

Varnishstat

Shows real-time statistics:

varnishstat

Key metrics:

  • MAIN.cache_hit: Number of cache hits.
  • MAIN.cache_miss: Number of cache misses.
  • MAIN.n_object: Number of objects in cache.
  • MAIN.s_resp_hdrbytes: Bytes sent in response headers.

Varnishlog

Shows detailed request logs:

varnishlog

Filter for specific URLs:

varnishlog -q 'ReqURL ~ "^/blog"'

Varnishtop

Shows top URLs, headers, or other fields:

varnishlog -i ReqURL | varnishtop

Debug headers

Add these headers to responses in vcl_deliver:

set resp.http.X-Cache = obj.hits > 0 ? "HIT" : "MISS";
set resp.http.X-Cache-TTL = obj.ttl;
set resp.http.X-Cache-Age = obj.age;

Now you can inspect cache status in browser dev tools or with curl -I.

How does Varnish compare to Nginx proxy cache and Cloudflare?

Varnish is not the only reverse proxy and caching solution. Here’s how it compares to Nginx proxy cache and Cloudflare:

FeatureVarnishNginx Proxy CacheCloudflare
Caching layerDedicated HTTP acceleratorBuilt into NginxGlobal CDN
Configuration languageVCL (Turing-complete)Nginx config (declarative)Dashboard + page rules
ESI supportYesNoNo
Cache invalidationHTTP PURGE, BANCache key invalidationCache purge API
Zero-copy memory cachingYesNoN/A
Typical hit rate80–95 %70–85 %60–80 %
Typical response time (cache hit)1–10 ms5–20 ms20–100 ms
Cost (self-hosted)FreeFreeFree tier + paid plans
Best forHigh-traffic apps with complex caching logicSimple proxy caching with NginxGlobal CDN with minimal setup

Verdict: If you need fine-grained control over caching logic and the highest hit rates, Varnish is the best choice. If you’re already using Nginx and want a simpler setup, Nginx proxy cache is sufficient. If you want a global CDN with minimal server configuration, Cloudflare is the easiest option—but it won’t give you the same hit rates or control as a self-hosted reverse proxy and caching layer.

Need fine-grained caching control?Yes → VarnishNoAlready using Nginx?Yes → Nginx proxy cacheNo → Cloudflare
Reverse proxy and caching decision tree: choose Varnish for control, Nginx for simplicity, or Cloudflare for global CDN.

How do you secure Varnish?

Varnish is not a web application firewall, but you can harden it:

Restrict access to the admin interface

Varnish’s admin interface listens on port 6082 by default. Bind it to localhost only:

sudo sed -i 's/-T 127.0.0.1:6082/-T 127.0.0.1:6082 -S \/etc\/varnish\/secret/' /etc/systemd/system/varnish.service

Create a secret file:

sudo sh -c 'echo "your-secret-password" > /etc/varnish/secret'
sudo chmod 600 /etc/varnish/secret

Reload systemd and restart Varnish:

sudo systemctl daemon-reload
sudo systemctl restart varnish

Use a firewall

Allow only ports 80 (HTTP) and 443 (HTTPS) from the public internet. Use UFW:

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 6081/tcp
sudo ufw deny 6082/tcp
sudo ufw enable

Rate limiting

Varnish 7.x supports rate limiting via the vmod_vsthrottle VMOD. Install it:

sudo apt install -y varnish-modules

Load the VMOD in your VCL:

import vsthrottle;

Add rate limiting in vcl_recv:

if (vsthrottle.is_denied("req_limit:" + client.ip, 100, 10s)) {
    return (synth(429, "Too Many Requests"));
}

HTTPS termination

Varnish does not support HTTPS natively. Terminate HTTPS at Nginx or Apache, then forward to Varnish on port 80. Here’s an Nginx config snippet for HTTPS termination:

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:80;
        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 $scheme;
    }
}

How do you scale Varnish for high traffic?

Varnish scales horizontally. Here’s how to handle traffic spikes:

Increase cache memory

Allocate more memory to the cache in the varnishd command:

-s malloc,4G

Monitor memory usage with varnishstat. If MAIN.s0.g_bytes approaches the limit, increase it.

Use multiple Varnish instances

Run multiple Varnish instances on different ports and load-balance them with Nginx:

upstream varnish {
    server 127.0.0.1:80;
    server 127.0.0.1:81;
    server 127.0.0.1:82;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://varnish;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Use a CDN in front of Varnish

For global traffic, put Cloudflare or another CDN in front of Varnish. Configure the CDN to cache static assets and forward dynamic requests to Varnish.

Monitor and auto-scale

Use tools like Prometheus and Grafana to monitor Varnish metrics. Set up alerts for high cache miss rates or high CPU usage. Use cloud auto-scaling to spin up more Varnish instances during traffic spikes.

ClientCDNLoad BalancerVarnish 1Varnish 2Varnish 3Backend PoolApp 1App 2App 3
Reverse proxy and caching with Varnish: horizontal scaling with multiple Varnish instances and a backend pool.

What are common pitfalls and how do you avoid them?

Varnish is powerful, but misconfiguration can cause subtle bugs. Here are the most common issues I’ve encountered on production deployments:

Cache stampede

When a cached object expires, multiple clients may simultaneously request the same uncached resource, overwhelming your backend. Mitigate it with:

  • Grace mode: serve stale content while fetching a fresh copy.
  • Pre-warming: use a cron job to refresh popular content before it expires.

Enable grace mode in VCL:

sub vcl_backend_response {
    set beresp.grace = 2h;
}

sub vcl_recv {
    if (req.http.Cache-Control ~ "no-cache") {
        return (pass);
    }
    return (hash);
}

sub vcl_hit {
    if (obj.ttl >= 0s) {
        return (deliver);
    }
    if (std.healthy(req.backend_hint)) {
        if (obj.ttl + obj.grace > 0s) {
            return (deliver);
        } else {
            return (fetch);
        }
    } else {
        if (obj.ttl + obj.grace > 0s) {
            return (deliver);
        } else {
            return (fetch);
        }
    }
}

Session leakage

If you cache responses that contain session cookies, you may leak user data. Always bypass cache for authenticated users:

sub vcl_recv {
    if (req.http.Cookie ~ "sessionid|auth_token") {
        return (pass);
    }
}

Incorrect Vary headers

The Vary header tells Varnish which request headers to consider when caching. If your backend sends Vary: User-Agent, Varnish will create a separate cache entry for every user agent, bloating the cache. Normalise the Vary header in VCL:

sub vcl_backend_response {
    if (beresp.http.Vary ~ "User-Agent") {
        set beresp.http.Vary = regsub(beresp.http.Vary, "(?i),?\s*User-Agent\s*,?", "");
        set beresp.http.Vary = regsub(beresp.http.Vary, "^,|,$", "");
    }
    if (beresp.http.Vary == "") {
        unset beresp.http.Vary;
    }
}

Stale content after deployment

After deploying new code, cached responses may serve stale content. Always purge the cache after deployment. Use a deployment hook:

# In your Deployer 7 recipe
after('deploy:symlink', 'varnish:purge');

task('varnish:purge', function () {
    run('curl -X PURGE http://127.0.0.1/');
});

Large objects in cache

Varnish is optimised for small, frequent objects. Large objects (e.g., video files) can fill the cache quickly. Exclude them from caching:

sub vcl_recv {
    if (req.url ~ "\.(mp4|mov|avi|mkv)$") {
        return (pass);
    }
}

How do you integrate Varnish with Laravel Forge or RunCloud?

If you’re using Laravel Forge or RunCloud to manage servers, you can still deploy Varnish as a reverse proxy and caching layer. Here’s how:

Laravel Forge

Forge provisions servers with Nginx. To add Varnish:

  1. SSH into your Forge server.
  2. Install Varnish:
sudo apt update
sudo apt install -y varnish
  1. Edit the Varnish systemd service to listen on port 80:
sudo systemctl edit --full varnish

Change the ExecStart line to:

ExecStart=/usr/sbin/varnishd \
    -a :80 \
    -f /etc/varnish/default.vcl \
    -s malloc,256m
  1. Move Nginx to port 8080:
sudo sed -i 's/listen 80/listen 8080/' /etc/nginx/sites-enabled/*
sudo systemctl restart nginx
  1. Reload systemd and restart Varnish:
sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo systemctl restart varnish
  1. Update your site’s Nginx config in Forge to listen on port 8080.
  2. Deploy your VCL rules to /etc/varnish/default.vcl and reload Varnish.

RunCloud

RunCloud uses Nginx as the default web server. To add Varnish:

  1. SSH into your RunCloud server.
  2. Install Varnish:
sudo apt update
sudo apt install -y varnish
  1. Edit the Varnish systemd service to listen on port 80:
sudo systemctl edit --full varnish

Change the ExecStart line to:

ExecStart=/usr/sbin/varnishd \
    -a :80 \
    -f /etc/varnish/default.vcl \
    -s malloc,256m
  1. Move Nginx to port 8080:
sudo sed -i 's/listen 80/listen 8080/' /etc/nginx-rc/conf.d/*
sudo systemctl restart nginx-rc
  1. Reload systemd and restart Varnish:
sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo systemctl restart varnish
  1. In the RunCloud dashboard, update your web application’s port to 8080.
  2. Deploy your VCL rules to /etc/varnish/default.vcl and reload Varnish.

Conclusion

Reverse proxy and caching with Varnish is a proven way to slash response times for Laravel, WordPress, and Symfony apps. By installing Varnish 7.x, configuring it as a reverse proxy on port 80, and writing framework-specific VCL rules, you can serve cached responses in microseconds instead of milliseconds. Handle cache invalidation with HTTP PURGE, monitor with varnishstat and varnishlog, and scale horizontally for high traffic.

If you’re running a production web system in Nepal or globally and want to accelerate it with Varnish, get in touch—I’ve deployed Varnish for eCommerce stores, legal-tech portals, and high-traffic APIs, and I can help you design a caching strategy that fits your application’s needs.

Frequently Asked Questions

A reverse proxy sits in front of web servers, forwarding client requests to the backend. Varnish is a high-performance HTTP accelerator designed specifically as a reverse proxy cache. It stores copies of responses from your web server (Apache/Nginx/PHP) and serves them directly to clients without hitting the backend, reducing load and latency. In production setups I've used, Varnish 7.4 sits between the internet and Nginx running PHP-FPM 8.3, handling all HTTP traffic before it reaches the application.

Varnish Cache is open-source and free to use under the BSD license. You only pay for server resources to run it. On a typical 2 vCPU, 4GB RAM Ubuntu 24.04 droplet (Rs 2,500/month, ~USD 19), Varnish adds negligible overhead. For enterprise features like Varnish Enterprise or Varnish Controller, pricing starts at ~USD 5,000/year, but most Laravel/WooCommerce sites run perfectly on the free version.

Use Varnish when you need sub-millisecond response times for anonymous traffic and can tolerate cache invalidation complexity. Varnish excels at caching full HTTP responses (HTML, JSON) with fine-grained TTL control and ESI support, while Nginx caching is simpler but lacks advanced purge logic. On a WooCommerce site I maintain, Varnish reduced product-page load times from 800ms to 40ms for repeat visitors, while Nginx caching alone only got us to 300ms.

First, add the official Varnish repository: curl -s https://packagecloud.io/install/repositories/varnishcache/varnish74/script.deb.sh | sudo bash. Then install: sudo apt install varnish. Configure the backend in /etc/varnish/default.vcl to point to your Nginx/PHP-FPM server (typically 127.0.0.1:8080). Set the Varnish listen port to 80 in /etc/default/varnish, then restart both Varnish and Nginx. Verify with varnishlog -g request -q 'ReqUrl eq "/"' during a test request.

Varnish itself doesn’t interact with PHP directly—it caches HTTP responses from your web server (Nginx/Apache). Your PHP version (8.2+ for Laravel 12) only matters for the backend application. However, ensure your PHP-FPM pool is properly sized to handle cache misses. On a production Laravel site, I run PHP 8.3.8 with pm.max_children=50 to handle traffic spikes when Varnish cache expires.

Use the varnishadm command or HTTP PURGE requests. For Laravel, install the spatie/laravel-varnish package (supports Varnish 6/7). Configure it with your Varnish admin IP and secret in config/varnish.php. Then purge a URL with Varnish::purge('https://example.com/product/123'). For WooCommerce, hook into product updates: add_action('save_post_product', function($post_id) { Varnish::purge("https://example.com/product/{$post_id}"); });.

The top three I’ve debugged: 1) Not whitelisting cookies like laravel_session or woocommerce_items_in_cart, causing logged-in users to see cached anonymous content. 2) Forgetting to set req.http.X-Forwarded-Proto = "https"; in vcl_recv, breaking Laravel’s secure URL generation. 3) Over-aggressive TTLs (e.g., 24h for product pages) without proper purge logic, leading to stale content. Always test with varnishlog -g request -q 'ReqUrl ~ "^/product"' during updates.

Varnish bypasses caching for requests with session cookies by default. In vcl_recv, you’ll see logic like if (req.http.Cookie ~ "laravel_session") { return(pass); }. For WooCommerce, add similar rules for woocommerce_items_in_cart. For logged-in users, use Varnish’s hit-for-pass feature: set beresp.ttl = 0s; and beresp.uncacheable = true; in vcl_backend_response. This ensures dynamic content never gets cached, while anonymous traffic benefits from full-page caching.

Yes, but carefully. Configure vcl_backend_response to cache JSON responses with proper Vary headers: if (beresp.http.Content-Type ~ "application/json") { set beresp.ttl = 5m; }. Use Cache-Control: public, max-age=300 in Laravel responses. For authenticated APIs, exclude endpoints with req.http.Authorization: if (req.http.Authorization) { return(pass); }. On a production API I maintain, this reduced average response time from 450ms to 20ms for cached endpoints, while preserving real-time data for authenticated requests.

Varnish is a self-hosted reverse proxy cache you control, while Cloudflare is a managed CDN with caching. Varnish offers microsecond-level cache invalidation, ESI support, and no monthly fees, but requires server management. Cloudflare provides global edge caching, DDoS protection, and zero-config setup, but lacks fine-grained purge control and charges for advanced features. For a Nepal-based WooCommerce site, I use Varnish for the origin server and Cloudflare for static assets, reducing bandwidth costs by 60%.

Use varnishstat for real-time metrics: varnishstat -1 -f MAIN.cache_hit,MAIN.cache_miss shows hit rate. For historical data, install varnish-agent and Grafana. Key metrics: cache_hit (aim for >80%), backend_fail (should be 0), and s_resp_bodybytes (bandwidth saved). On a production site, I set up a cron job to log varnishstat -1 to a file every 5 minutes, then graph it with Prometheus. For Laravel, the spatie/laravel-varnish package includes a hit-rate middleware you can log to your application metrics.

Varnish can expose sensitive data if misconfigured. Mitigations: 1) Restrict admin access: bind varnishadm to 127.0.0.1 and use a strong secret in /etc/varnish/secret. 2) Disable PURGE from external IPs in vcl_recv: if (req.method == "PURGE" && client.ip !~ admin_network) { return(synth(405)); }. 3) Sanitize headers: unset req.http.X-Forwarded-For; in vcl_recv to prevent spoofing. 4) Rate-limit with vmod-ratelimit. On a production site, I also use UFW to block all ports except 80/443 and the Varnish admin port (restricted to internal IPs).

Use Varnish’s Vary header to cache separate versions per currency. In vcl_hash, add: hash_data(req.http.X-Currency);. In WooCommerce, set the currency cookie (woocommerce_currency) before Varnish processes the request. Configure vcl_backend_response to respect the Vary header: if (beresp.http.Vary) { set beresp.http.Vary = beresp.http.Vary + ", X-Currency"; }. For NPR/USD switching, this ensures users see prices in their selected currency without cache collisions. Test with varnishlog -g request -q 'ReqUrl ~ "^/shop"' while switching currencies.

Use a zero-downtime deployment strategy with cache warming. In your Deployer 7 script, add a task to purge the entire cache after symlink swap: task('deploy:cache:purge', function() { run('varnishadm "ban req.url ~ /"'); });. For Laravel, also warm the cache by crawling key URLs: run('curl -s http://localhost/ > /dev/null');. On a production site, I schedule a cache warm cron job to run 5 minutes after deploy, reducing cache-miss spikes. For critical pages, use Varnish’s saint mode to serve stale content during backend failures.

Check three things: 1) Backend health: varnishadm backend.list should show "healthy". 2) Logs: varnishlog -g request -q 'RespStatus == 503' shows the exact error. 3) Resource limits: varnishstat -1 | grep n_object shows cache memory usage (default 256MB may be too low). Common fixes: increase beresp.ttl in vcl_backend_response, adjust PHP-FPM pm.max_children, or increase Varnish’s -s malloc parameter. On a production site, I once debugged 503s caused by a misconfigured Nginx upstream timeout—Varnish was retrying the backend too quickly. The fix was to set first_byte_timeout = 60s; in vcl_backend_response.

Share this article

Quick Contact Options
Choose how you want to connect me: