Reverse Proxy and Caching with Varnish

Khimananda Oli 7 min read Database
Reverse Proxy and Caching with Varnish

By Khimananda Oli | Last reviewed: August 2026

Slow dynamic applications frustrate users and inflate cloud bills by forcing expensive compute resources to regenerate identical content. Implementing reverse proxy and caching with Varnish solves this by serving cached responses from memory in microseconds, bypassing PHP or application logic entirely for repeat requests. If you are already running a standard LEMP stack, understanding where Varnish fits is critical before attempting complex optimizations like those in our guide on Laravel performance optimization techniques.

Client BrowserVarnish Cache(Port 6081)Backend App(Nginx/Apache)HTTP ReqCache Miss
High-level architecture of reverse proxy and caching with Varnish sitting between clients and the origin backend

How does reverse proxy and caching with Varnish actually work?

Varnish operates as an HTTP accelerator designed specifically for heavy-load environments. Unlike general-purpose web servers, it stores cached objects entirely in virtual memory managed by the kernel, avoiding disk I/O bottlenecks. When a request arrives, Varnish computes a hash based on the URL and host header, then checks its storage. A hit returns the object immediately; a miss triggers a fetch from the defined backend.

This architecture makes reverse proxy and caching with Varnish exceptionally fast for read-heavy workloads like CMS platforms, e-commerce catalogs, and API gateways. However, it requires careful configuration because Varnish speaks only HTTP. It does not terminate TLS natively. In production, you almost always place Nginx or HAProxy in front to handle SSL termination, forwarding decrypted traffic to Varnish on port 6081. This separation of concerns keeps the caching layer simple and focused purely on acceleration.

A common mistake I see teams make is treating Varnish as a drop-in replacement for Nginx. It is not. Think of it as a specialized co-processor for HTTP delivery. Your existing web server configuration remains relevant, but now handles only uncached or private requests. For teams setting up fresh infrastructure, our LEMP stack setup guide covers the foundational Nginx layer that typically sits upstream.

How do you install and configure Varnish on Ubuntu 24.04?

Installation on modern Ubuntu systems uses the official packages. Avoid outdated tutorials referencing older init scripts; current versions use systemd exclusively.

  1. Install the package: sudo apt update && sudo apt install varnish
  2. Edit the systemd service to set memory allocation. Open /etc/systemd/system/varnish.service.d/custom.conf:
    [Service]
    ExecStart=
    ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F -a :6081 -T localhost:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,1G
    The -s malloc,1G flag allocates 1GB of RAM for cache storage. Adjust based on available memory and content size.
  3. Configure your backend in /etc/varnish/default.vcl:
    vcl 4.1;
    
    backend default {
        .host = "127.0.0.1";
        .port = "8080";
        .connect_timeout = 5s;
        .first_byte_timeout = 90s;
        .between_bytes_timeout = 2s;
    }
    This tells Varnish where to find your application when a cache miss occurs.
  4. Reload systemd and restart: sudo systemctl daemon-reload && sudo systemctl restart varnish

Verify operation with varnishlog -g request. You should see incoming requests and their cache status (hit, miss, pass). If everything shows "pass", your VCL is likely bypassing cache due to cookies or headers — addressed in the next section.

Request Arrivesvcl_recvCache HITCache MISSFetch BackendLookup SuccessNot Found / Pass
Varnish request lifecycle showing vcl_recv decision points for reverse proxy and caching with Varnish

How do you write VCL to handle cookies and cache invalidation safely?

The default VCL is conservative. It passes (skips cache) for any request containing Cookie or Authorization headers. For dynamic apps, this means zero caching unless you explicitly normalize these headers. Here is a practical pattern for a typical CMS or Laravel app:

sub vcl_recv {
    # Remove tracking cookies but preserve session auth
    if (req.http.Cookie) {
        set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_ga|_gid|utm_[^=]+)=[^;]*", "");
        set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
        if (req.http.Cookie == "") {
            unset req.http.Cookie;
        }
    }

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

    # Normalize Accept-Encoding to improve hit rate
    if (req.http.Accept-Encoding) {
        if (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elsif (req.http.Accept-Encoding ~ "deflate") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            unset req.http.Accept-Encoding;
        }
    }
}

sub vcl_backend_response {
    # Respect backend TTL but enforce minimum for public pages
    if (beresp.ttl < 120s && !bereq.http.Authorization) {
        set beresp.ttl = 120s;
    }

    # Don't cache error responses
    if (beresp.status >= 400) {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }
}

Invalidation is equally important. Use BAN requests rather than PURGE for pattern-based invalidation. This lets you clear all product pages at once instead of individual URLs:

# From your deployment script or CI pipeline
curl -X BAN http://localhost:6081/ \
  -H "X-Purge-Regex: /products/.*" \
  -H "X-Ban-Key: secret-token"

Always secure the BAN/PURGE interface. Never expose port 6082 publicly. Restrict access via firewall rules or VCL ACLs. Teams deploying via automated pipelines should integrate cache clearing into their release process, similar to the strategies covered in our zero-downtime deployment guide.

How does Varnish compare to Nginx FastCGI cache and Redis?

Choosing the right caching layer depends on your specific workload characteristics. Each tool has distinct strengths.

FeatureVarnishNginx FastCGI CacheRedis
Primary Use CaseFull-page HTTP cachingPHP/application response cachingObject/data fragment caching
StorageRAM (malloc)Disk (SSD recommended)RAM + optional persistence
TLS SupportNo (needs frontend proxy)Yes (native)No (application-level)
Configuration LanguageVCL (stateful, powerful)Directives (declarative)Application code
Invalidation GranularityBAN/PURGE by regex/tagPURGE by key/wildcardKey/pattern/delete
Best ForHigh-traffic public sitesSimpler stacks, no extra daemonSession, query, computed data

In practice, many high-performance stacks combine these. Varnish handles full-page delivery, Redis stores session and query results, and Nginx manages TLS plus static assets. The decision isn't either/or — it's about layering appropriately. If your traffic is under 1k RPS and mostly authenticated, Nginx cache alone may suffice. If you serve millions of public page views daily, Varnish's purpose-built HTTP state machine delivers measurably lower p99 latency.

0ms500msDirect BackendAvg 320msVarnish HitAvg 2msVarnish MissAvg 340msResponse Time Comparison
Latency impact of reverse proxy and caching with Varnish showing dramatic improvement on cache hits

How do you monitor Varnish cache hit rates and tune performance?

Blindly deploying Varnish without observability leads to silent failures. You must track hit rate, memory usage, and backend health continuously.

  • Real-time stats: varnishstat -1 gives a snapshot. Monitor MAIN.cache_hit vs MAIN.cache_miss. A healthy public site should sustain >85% hit rate after warmup.
  • Prometheus integration: Use varnish_exporter to expose metrics. Alert on hit rate dropping below threshold or backend errors spiking. Our Prometheus monitoring guide covers dashboard patterns applicable here.
  • Memory tuning: Watch SMA.s0.g_alloc and SMA.s0.g_space. If space consistently nears zero, increase malloc allocation or review TTL policies. Fragmentation can occur with highly variable object sizes; consider malloc over file storage for predictable workloads.
  • Log analysis: varnishncsa outputs combined log format. Pipe to your centralized logging stack to correlate cache behavior with user experience metrics. Low hit rates often trace back to unnormalized query strings or rogue cookies.

Performance tuning extends beyond Varnish itself. Ensure your backend sets appropriate Cache-Control headers. Varnish respects max-age and s-maxage; missing headers force fallback TTLs. Also verify TCP keepalives and connection pooling between Varnish and backend — each new connection adds latency on misses.

Implementing Reverse Proxy and Caching with Varnish in Production

Effective reverse proxy and caching with Varnish transforms application responsiveness and reduces infrastructure costs, but success depends on methodical implementation. Start with accurate backend definitions, normalize headers aggressively, validate cache behavior with real traffic patterns, and instrument everything before going live. Treat VCL as production code: version it, test it, review it. The payoff is a system that serves users faster while costing less to operate.

If you need help architecting a caching strategy tailored to your traffic profile or compliance requirements, reach out to discuss your infrastructure. I help teams build performant, audit-ready systems that scale without surprise.

Frequently Asked Questions

Varnish is an HTTP accelerator that sits in front of web servers, caching content in memory. It intercepts requests, serves cached responses instantly, and only forwards cache misses to the backend, dramatically reducing server load and latency for dynamic sites.

Run apt update then apt install varnish. Edit /etc/varnish/default.vcl to define your backend host and port. Configure systemd to listen on port 80 by editing /etc/systemd/system/varnish.service.d/override.conf, then restart the service with systemctl restart varnish.

No.

Place HAProxy or Nginx in front of Varnish to handle TLS termination. These proxies decrypt traffic and forward plain HTTP to Varnish on localhost. This separation maintains Varnish performance while providing modern SSL/TLS support required for production environments in 2026.

In vcl_recv, check for Authorization headers or session cookies using req.http.Authorization or req.http.Cookie. Return pass when detected to skip caching entirely. This prevents serving stale or private content to logged-in users while keeping anonymous traffic fully cached.

Varnish specializes purely in HTTP caching with advanced VCL logic and superior hit rates for dynamic content. Nginx offers basic caching alongside web serving but lacks Varnish's sophisticated invalidation, edge-side includes, and memory management optimized specifically for high-traffic reverse proxy scenarios.

Allocate 50-75% of available system RAM for -s malloc parameter. Monitor actual usage with varnishstat -f SMA.s0.g_bytes. Oversizing wastes memory while undersizing causes excessive evictions. Adjust based on object count, average size, and observed cache hit ratio over several days.

Send HTTP PURGE requests to Varnish using curl or application code. Configure acl blocks in VCL to restrict purge access to trusted IPs. For wildcard purges, use ban expressions like ban("req.url ~ /products/") which mark objects invalid without immediate removal overhead.

Check for varying headers causing cache fragmentation using varnishlog -g request. Normalize Accept-Encoding, strip unnecessary query parameters, and ensure consistent Host headers. Verify backend isn't sending Cache-Control: private or no-cache. Analyze miss reasons with varnishstat to identify specific bottlenecks.

Yes, but cautiously. Override default behavior in vcl_recv by returning hash instead of pass for specific POST endpoints. Implement custom cache keys including relevant body parameters. Only cache idempotent API responses where identical requests yield identical results, never user-specific mutations.

Use varnishstat for real-time metrics like hit rate, memory usage, and backend connections. Export stats to Prometheus via varnish_exporter for Grafana dashboards. Monitor varnishlog selectively during debugging. Set alerts on cache hit ratio dropping below 85% or backend connection spikes indicating cache thrashing.

Add X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Strict-Transport-Security in vcl_deliver. Strip Server and X-Powered-By headers to reduce fingerprinting. Never cache responses containing sensitive data. Validate that upstream applications set appropriate Cache-Control directives before Varnish stores objects in shared cache.

Define probe blocks in VCL specifying URL, interval, timeout, and expected status codes. Varnish marks backends sick after consecutive failures and routes traffic to healthy alternatives. Configure .window and .threshold parameters appropriately. Monitor backend_health changes via varnishlog to detect infrastructure issues before users experience errors.

Yes.

Use Laravel's cache tags to generate corresponding Varnish ban expressions via webhook or queue job. When content updates, trigger bans matching tag patterns. Configure surrogate-key headers in responses so Varnish can efficiently invalidate related objects without full cache flushes or manual intervention.