
August 23, 2026
11 min read
Table of Contents
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.
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:
- Allow PURGE requests in VCL.
- 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:
| Feature | Varnish | Nginx Proxy Cache | Cloudflare |
|---|---|---|---|
| Caching layer | Dedicated HTTP accelerator | Built into Nginx | Global CDN |
| Configuration language | VCL (Turing-complete) | Nginx config (declarative) | Dashboard + page rules |
| ESI support | Yes | No | No |
| Cache invalidation | HTTP PURGE, BAN | Cache key invalidation | Cache purge API |
| Zero-copy memory caching | Yes | No | N/A |
| Typical hit rate | 80–95 % | 70–85 % | 60–80 % |
| Typical response time (cache hit) | 1–10 ms | 5–20 ms | 20–100 ms |
| Cost (self-hosted) | Free | Free | Free tier + paid plans |
| Best for | High-traffic apps with complex caching logic | Simple proxy caching with Nginx | Global 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.
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.
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:
- SSH into your Forge server.
- Install Varnish:
sudo apt update
sudo apt install -y varnish
- 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
- Move Nginx to port 8080:
sudo sed -i 's/listen 80/listen 8080/' /etc/nginx/sites-enabled/*
sudo systemctl restart nginx
- Reload systemd and restart Varnish:
sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo systemctl restart varnish
- Update your site’s Nginx config in Forge to listen on port 8080.
- Deploy your VCL rules to
/etc/varnish/default.vcland reload Varnish.
RunCloud
RunCloud uses Nginx as the default web server. To add Varnish:
- SSH into your RunCloud server.
- Install Varnish:
sudo apt update
sudo apt install -y varnish
- 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
- Move Nginx to port 8080:
sudo sed -i 's/listen 80/listen 8080/' /etc/nginx-rc/conf.d/*
sudo systemctl restart nginx-rc
- Reload systemd and restart Varnish:
sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo systemctl restart varnish
- In the RunCloud dashboard, update your web application’s port to 8080.
- Deploy your VCL rules to
/etc/varnish/default.vcland 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.

