Varnish HTTP Cache in Front of Nginx: Production Performance Guide
Performance

Varnish HTTP Cache in Front of Nginx: Production Performance Guide

  • Author :Liam K.
  • Date :July 01, 2026
  • Time :28 minutes

Varnish is a dedicated HTTP accelerator designed for caching at scale. Unlike Nginx proxy cache, Varnish uses VCL (Varnish Configuration Language) for fine-grained control over what gets cached, for how long, and how cache keys are built. Placing Varnish in front of Nginx and your application can reduce backend load by 60–90% for read-heavy workloads like blogs, APIs with public endpoints, and static asset delivery.

This guide covers a classic two-tier architecture: clients hit Varnish on port 80/443 (or port 80 with TLS terminated at Nginx behind Varnish), Varnish serves cached responses or fetches from Nginx on localhost, and Nginx proxies to your application. The pattern works for WordPress, Laravel, Node.js APIs, and any HTTP backend that sends correct cache headers.

Request Flow

  • Cache HIT — Varnish returns the stored object without contacting the backend.
  • Cache MISS — Varnish forwards the request to Nginx, stores the response, then delivers it.
  • PURGE/BAN — application or admin triggers invalidation when content changes.
  • Pass — VCL marks certain requests (POST, authenticated) to always reach the backend.

Prerequisites

  • Ubuntu 22.04+ or Debian 12 server
  • Nginx already serving your application on localhost (e.g. port 8080)
  • Sufficient RAM — plan 1–4 GB for Varnish cache storage depending on traffic
  • Understanding of which URLs are safe to cache (no user-specific HTML without Vary headers)

Step 1: Install Varnish 7

bash
curl -fsSL https://packagecloud.io/varnishcache/varnish70/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/varnish.gpg
echo "deb [signed-by=/usr/share/keyrings/varnish.gpg] https://packagecloud.io/varnishcache/varnish70/ubuntu/ jammy main" | sudo tee /etc/apt/sources.list.d/varnish.list
sudo apt update
sudo apt install -y varnish
varnishd -V

Step 2: Configure Nginx as Backend

Move Nginx to an internal port. Varnish becomes the public-facing entry point on port 80. Add cache-friendly headers from the backend for dynamic content you want Varnish to respect.

nginx
# /etc/nginx/sites-available/app-backend.conf
server {
    listen 127.0.0.1:8080;
    server_name app.example.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
[...]
Command truncated. Copy to view full command.

Step 3: Write Production VCL

bash
sudo tee /etc/varnish/default.vcl >/dev/null <<'EOF'
vcl 4.1;
backend nginx {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 5s;
    .first_byte_timeout = 30s;
    .between_bytes_timeout = 30s;
[...]
Command truncated. Copy to view full command.

Step 4: Configure Varnish systemd Service

bash
# Edit /etc/default/varnish:
# VARNISH_LISTEN_ADDRESS=:80
# VARNISH_STORAGE="malloc,2G"
# VARNISH_VCL_CONF=/etc/varnish/default.vcl

sudo systemctl enable varnish
sudo varnishd -C -f /etc/varnish/default.vcl
sudo systemctl restart varnish
sudo systemctl status varnish --no-pager

Step 5: Verify Cache Behavior

bash
curl -I http://app.example.com/
# First request: X-Cache: MISS

curl -I http://app.example.com/
# Second request: X-Cache: HIT

varnishstat -1 | grep -E 'cache_hit|cache_miss'
varnishlog -q 'VCL_call eq DELIVER'

Step 6: Purge Cached Content

When content changes, purge specific URLs or use bans for pattern-based invalidation. Integrate purges into your CMS deploy hook or CI pipeline.

bash
# Purge a single URL:
curl -X PURGE http://app.example.com/blog/my-post

# Ban all blog posts (regex ban):
varnishadm "ban req.url ~ ^/blog/"

# From application deploy script:
curl -X PURGE -H "Host: app.example.com" http://127.0.0.1/blog/my-post

Step 7: Add TLS with Nginx in Front (Optional)

Some teams terminate TLS at Nginx on port 443 and proxy to Varnish on port 80 internally. Others use Hitch or Caddy for TLS and forward to Varnish. Pick one TLS termination point and keep the chain consistent.

nginx
# Nginx TLS terminator → Varnish → Nginx backend
server {
    listen 443 ssl http2;
    server_name app.example.com;
    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:80;
[...]
Command truncated. Copy to view full command.

Grace Mode and Backend Failures

Grace mode (beresp.grace) lets Varnish serve slightly stale content when the backend is slow or unavailable. This is valuable during deploys and transient outages. Set grace per content type — short for news feeds, longer for static marketing pages.

Common Mistakes

  • Caching authenticated pages because cookies were not stripped or checked in vcl_recv.
  • No purge mechanism — editors see stale content for hours after publishing.
  • Cache key ignores Vary headers, serving wrong language or encoding.
  • Allocating too little malloc storage — high miss rate under load despite correct VCL.
  • PURGE ACL too broad — attackers can flush your entire cache from the internet.

Production Checklist

  • Target 80%+ cache hit rate for public read endpoints after warm-up.
  • Monitor cache_hit / cache_miss ratio with Prometheus varnish_exporter.
  • Document which URL patterns are cached, passed, or banned.
  • Test purge flows in staging before relying on them in production.
  • Load-test with and without cache to validate backend protection.

"HTTP caching is a contract between your application headers and your VCL logic — if either side lies about cacheability, users see stale data or no benefit at all."

Technical Author

Technical Author - Liam K.
Liam K.

System administrator and technical writer specializing in server infrastructure, security and deployment. Creating comprehensive guides to help you master server administration.