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.

Configure a Caching Reverse Proxy

By Kokil Thapa | Last reviewed: September 2026

Every production site eventually hits the same wall: PHP workers, database queries, and disk I/O cannot serve every page on every request. When you configure a caching reverse proxy, you put a fast HTTP cache in front of your app so repeat visitors get static-speed responses without rewriting your codebase. On real client projects—WooCommerce florists, Laravel booking portals, legal-tech sites—I treat the reverse proxy as part of application architecture, not a hosting afterthought. This guide walks through Nginx and Varnish setups, header contracts, purge workflows, and the mistakes that silently poison cache hit rates. For broader context, see our notes on reverse proxy and caching with Varnish and improving web performance with caching strategies.

What Is a Caching Reverse Proxy and When Should You Use One?

A caching reverse proxy sits between browsers and your origin server. It terminates HTTP, stores responses keyed by URL and headers, and serves cached copies on cache hits. Your PHP-FPM pool, MySQL queries, and Blade rendering only run on cache misses.

You need this layer when anonymous traffic dominates, HTML pages change infrequently, or your origin struggles under burst load. Dashain and Tihar traffic spikes on Nepal eCommerce sites are a classic case. A well-tuned proxy absorbs the surge while checkout and account pages still reach PHP untouched.

You should skip full-page proxy caching when every response is personalised, when sessions leak into HTML, or when stale content creates legal or financial risk. Document portals and authenticated client areas on platforms like Mijar Law Associates typically cache only static assets, not HTML bodies.

Caching Reverse Proxy TopologyBrowserHTTPS requestReverse ProxyNginx / VarnishTLS + HTTP cacheOriginPHP / LaravelDisk / RAM CacheHit = no origin callConfigure a caching reverse proxy at the edge, not inside PHP
How a caching reverse proxy sits between clients and your Laravel or WordPress origin on Ubuntu production servers.

The proxy handles three jobs at once. It terminates SSL, applies caching rules, and forwards uncacheable requests unchanged. Redis or application caches still help on the origin side. They solve different problems, as covered in Redis caching for Laravel and database query caching strategies.

How Do You Choose Between Nginx and Varnish for HTTP Caching?

Both tools can configure a caching reverse proxy. The right pick depends on your stack, team skills, and traffic shape. Nginx is already on most Ubuntu servers I maintain for Laravel and WordPress. Adding proxy_cache avoids a second daemon.

Varnish is a dedicated HTTP cache with a powerful VCL language. It excels at high-volume anonymous HTML, ESI fragments, and fine-grained cache logic. The trade-off is another service to patch, monitor, and secure behind your firewall rules from UFW on Ubuntu.

CriterionNginx proxy_cacheVarnish
Setup complexityLow if Nginx is already fronting PHP-FPMMedium — separate VCL workflow
Cache logicDirective-based, good for common casesVCL scripting, very flexible
TLS terminationNative, well documentedNeeds hitch, HAProxy, or Nginx in front
Purge APIproxy_cache_purge module or cache path deleteBuilt-in PURGE and ban/lurker
Best fitLaravel 12/13, WordPress 7.1, mixed static + APIHigh-traffic HTML, aggressive edge caching
Ops footprintOne process many sites already useExtra service + monitoring

On sister sites sharing a Deployer 7 pipeline—legal portals, translation sites—I default to Nginx unless traffic data proves Varnish is worth the ops cost. For a deep Varnish walkthrough, read reverse proxy and caching with Varnish. For Nginx-only setups, see set up a reverse proxy with Nginx.

Cache Hit vs Miss FlowIncoming GETValid cache key?CACHE HITServe from diskCACHE MISSFetch originYesNoStore if cacheable
Decision path when you configure a caching reverse proxy: valid keys return instantly; misses reach PHP-FPM then populate the cache.

How Do You Configure Nginx as a Caching Reverse Proxy?

Nginx caching lives in the http context and per-location blocks. Start with a dedicated cache zone on fast disk. SSD paths under /var/cache/nginx work well on Ubuntu 22/24 servers running PHP 8.3 or 8.5.

Define the cache zone and upstream

# /etc/nginx/nginx.conf (http block)
proxy_cache_path /var/cache/nginx/laravel
    levels=1:2
    keys_zone=LARAVEL:100m
    inactive=60m
    max_size=2g
    use_temp_path=off;

upstream laravel_upstream {
    server 127.0.0.1:8080;
    keepalive 32;
}

The keys_zone name and size control how many cache keys fit in shared memory. One megabyte holds roughly eight thousand keys. Size it for your URL count, not wishful thinking.

Enable proxy_cache on public pages

# /etc/nginx/sites-available/example.com
server {
    listen 443 ssl http2;
    server_name example.com;

    location / {
        proxy_pass http://laravel_upstream;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;

        proxy_cache LARAVEL;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 301 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503;
        proxy_cache_lock on;
        add_header X-Cache-Status $upstream_cache_status always;

        proxy_no_cache $cookie_session $http_authorization;
        proxy_cache_bypass $cookie_session $http_authorization;
    }

    location ~ ^/(admin|dashboard|api|login) {
        proxy_pass http://laravel_upstream;
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        include proxy_params;
    }
}

That pattern mirrors what I deploy on production Laravel applications. Public GET pages cache. Authenticated zones, APIs, and login routes bypass the cache entirely. The X-Cache-Status header makes debugging painless during testing and optimization work.

Respect origin Cache-Control headers

By default, Nginx can ignore upstream headers unless you tell it otherwise. Add these directives when your app sends proper HTTP caching metadata, as described in the official Nginx proxy module documentation:

proxy_cache_valid any 0;          # disable blanket TTL
proxy_ignore_headers Set-Cookie;  # only if app never sets cookies on cacheable pages
proxy_cache_revalidate on;

Blindly ignoring Set-Cookie is dangerous. If PHP sets a session cookie on a homepage, you will cache personalised HTML for everyone. Fix the app first. Then tune the proxy.

Pick Your Caching Reverse ProxyNginx proxy_cache+ Already on most stacks+ TLS in one config file+ Good for Laravel + WP- Less flexible purge logic- VCL not availableDefault choiceVarnish Cache+ Dedicated HTTP engine+ VCL + ban/lurker purge+ High-traffic HTML wins- Extra daemon to run- TLS needs front proxyHigh traffic HTML
Decision guide when you configure a caching reverse proxy: Nginx for typical PHP stacks, Varnish for aggressive edge HTML caching.

Minimal Varnish VCL for the same origin

If you outgrow Nginx caching, Varnish VCL expresses the same rules more explicitly. The Varnish Cache documentation remains the authoritative reference:

# /etc/varnish/default.vcl
vcl 4.1;

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

sub vcl_recv {
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }
    if (req.url ~ "^/(admin|dashboard|api|login)") {
        return (pass);
    }
    if (req.http.Cookie ~ "session") {
        return (pass);
    }
    unset req.http.Cookie;
    return (hash);
}

sub vcl_backend_response {
    if (beresp.http.Cache-Control ~ "private|no-store|no-cache") {
        set beresp.ttl = 0s;
        return (deliver);
    }
    if (beresp.status == 200) {
        set beresp.ttl = 10m;
    }
}

Varnish strips cookies on cacheable pages at the edge. Your origin must not depend on cookies for anonymous HTML. That contract is non-negotiable.

How Should Laravel and PHP Apps Set Cache Headers for a Reverse Proxy?

The proxy only caches what the origin marks cacheable. Laravel 12 and 13 apps should set headers in middleware or controllers, not hope the proxy guesses correctly. Application-level caching from Laravel config, route, and view cache complements—but does not replace—the edge layer.

Middleware example for anonymous pages

<?php
// app/Http/Middleware/CachePublicPages.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CachePublicPages
{
    public function handle(Request $request, Closure $next)
    {
        $response = $next($request);

        if ($request->user()) {
            return $response;
        }

        return $response->header('Cache-Control', 'public, max-age=600, s-maxage=600')
            ->header('Vary', 'Accept-Encoding');
    }
}

Use s-maxage for shared caches like Nginx and Varnish. Use max-age for browser caching. Keep them aligned unless you have a reason to split behaviour. For API responses, ETag patterns from API caching with ETag and Last-Modified work better than long TTL HTML caching.

Headers that must never appear on cacheable pages

  • Set-Cookie on anonymous HTML — poisons the cache key or forces bypass
  • Cache-Control: private — tells shared caches to skip storage
  • Vary: Cookie — explodes cache cardinality until disk fills
  • Missing Vary: Accept-Encoding — serves gzip/br garbage to wrong clients

On WooCommerce and high-traffic storefronts like Petals Qatar, product pages can cache at the edge while cart, checkout, and my-account routes pass through. WooCommerce 11.1 sets cookies early; exclude those URL prefixes explicitly in Nginx location blocks.

WordPress 7.1 admin bar cookies create the same headache. Either disable the bar for anonymous users or bypass cache when the cookie exists. There is no magic directive that fixes bad session hygiene.

How Do You Purge and Invalidate Proxy Cache Safely?

Stale content is the main complaint after you configure a caching reverse proxy. Plan purge mechanics before launch, not after an editor publishes a corrected legal fee table.

  1. URL purge on publish — Laravel model observers call an internal purge endpoint when content changes.
  2. Prefix ban (Varnish)ban("req.url ~ ^/blog/") clears an entire section.
  3. TTL fallback — short s-maxage limits staleness when purge fails.
  4. Deploy hook — GitLab CI or Deployer 7 tasks flush edge cache after symlink swap, same as PHP-FPM reload.

I wire purge calls into Deployer recipes on shared EC2 infrastructure alongside opcache clears. A missed purge after deploy is a common reason editors see old content while developers see fresh output.

Secure Nginx purge location

location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    proxy_cache_purge LARAVEL "$scheme$request_method$host$1";
}

The proxy_cache_purge module ships with Nginx Plus or third-party builds. On stock Ubuntu Nginx, delete files under the cache path or use a small internal PHP/Laravel artisan command that maps URLs to cache keys. Never expose unauthenticated purge URLs to the public internet.

Cache Purge WorkflowEditor savesCMS / LaravelObserverPurge hookDeployer CIPost-deploy flushReverse ProxyKeys removedCombine event purge + deploy flush for reliable invalidation
Production purge path after you configure a caching reverse proxy: content events and deploy hooks both invalidate stale HTML.

For high-traffic sites, read caching strategies for high traffic sites before setting multi-hour TTL values. Legal and pricing pages deserve shorter TTL or active purge because stale information creates support load.

What Production Mistakes Break Caching Reverse Proxy Setups?

Most failed caching projects are header and routing problems, not missing directives. These recur on nearly every audit I run during speed optimization engagements.

Session cookies on every response

Laravel starts sessions globally by default. Anonymous visitors get a laravel_session cookie on the homepage. Nginx sees the cookie and bypasses cache. Move session middleware to routes that need it, or use the web group only on authenticated sections.

Query string cache explosion

UTM parameters create unique cache keys for identical content. Normalize URLs at the proxy:

proxy_cache_key "$scheme$request_method$host$uri";

Using $request_uri includes query strings. Switch to $uri when marketing params should not fragment the cache. Be careful with legitimate filters on booking and search pages where query strings change content.

HTTPS mixed schemes in cache keys

If health checks hit HTTP and users hit HTTPS, you duplicate cache entries. Terminate TLS at the proxy and talk HTTP to localhost upstream only.

Forgetting cache on static assets

Proxy HTML caching helps TTFB. Static assets still need long browser TTL via Nginx expires or a CDN. Combine both layers for full effect, as outlined on our web development service page.

No monitoring on hit rate

Parse Nginx access logs for X-Cache-Status values. Hit rates below 60% on anonymous pages signal misconfigured bypass rules. Use JSON formatter tools when inspecting webhook payloads from monitoring agents—you will thank yourself at 2 AM.

Redis 8.10 on the origin still helps with fragment or query caching per Redis caching patterns. The reverse proxy and Redis solve different layers. Use both intentionally.

Hard refresh in Chrome (Ctrl+Shift+R) bypasses browser cache but not always the proxy. Test with curl -I and a clean URL. Compare response headers against the HTTP caching rules in RFC 7234 when debugging unexpected MISS statuses.

Key Takeaways

  • Configure a caching reverse proxy at Nginx or Varnish—not inside PHP—so anonymous GET traffic never touches PHP-FPM on cache hits.
  • Bypass cache for login, cart, dashboard, and API routes; cache only responses with clean headers and no session cookies.
  • Set Cache-Control: public, s-maxage=… and Vary: Accept-Encoding in Laravel middleware so the proxy knows what to store.
  • Wire purge hooks into content saves and Deployer deploys; short TTL is your safety net when purge fails.
  • Monitor X-Cache-Status hit rates and fix cookie or query-string issues before chasing micro-optimisations.
  • Combine edge caching with Redis and query optimisation for full-stack performance on high-traffic Nepal and global sites.

People Also Ask

Does a caching reverse proxy replace Redis in Laravel?

No. A reverse proxy caches full HTTP responses at the edge. Redis caches data fragments, sessions, and queue payloads inside the application. They complement each other. Use both on production Laravel 12/13 stacks when traffic justifies the ops overhead.

How long should s-maxage be for blog and product pages?

Ten to thirty minutes is a sensible starting range for content that changes occasionally. Pair it with active purge on publish. Legal, pricing, and inventory pages need shorter TTL or event-driven invalidation because stale data costs real money and trust.

Can I cache WordPress pages with WooCommerce active?

Yes, but only URLs that never set cart or session cookies. Exclude /cart, /checkout, /my-account, and wp-admin in Nginx locations. Test with curl and inspect Set-Cookie headers before enabling aggressive TTL.

Is Varnish still relevant in 2026 with Nginx and CDNs?

Yes for origin-side edge caching when you control the server and serve heavy anonymous HTML. CDNs add another layer in front. Many teams use CDN → Nginx → PHP. Varnish earns its place when Nginx cache logic becomes too complex for directives alone.

Ship Faster Pages With a Proper Edge Cache

When you configure a caching reverse proxy correctly, your origin stops drowning in repeat work. Public pages fly. Editors publish confidently because purge hooks clear stale HTML. Authenticated workflows stay untouched. That is the balance production sites need—especially seasonal Nepal businesses where traffic spikes are predictable but budgets are not.

If you want help auditing headers, writing Nginx cache rules, or integrating purge into a Deployer pipeline, contact us or explore Linux system administration and support and maintenance options. For related reading, browse Traefik for Docker workloads, review work on Adventure Himalaya Nepal, or start from the homepage and about page to see how edge caching fits full-stack delivery.

Frequently Asked Questions

A caching reverse proxy sits between browsers and your origin server, terminates HTTP, stores responses keyed by URL and headers, and serves cached copies on hits so PHP-FPM, MySQL, and rendering only run on cache misses.

Use one when anonymous traffic dominates, HTML changes infrequently, or your origin struggles under burst load—Dashain and Tihar spikes on Nepal eCommerce are a classic case. Skip full-page caching when every response is personalised, sessions leak into HTML, or stale content creates legal or financial risk. Document portals and authenticated client areas typically cache static assets only, not HTML bodies.

No. The proxy caches full HTTP responses at the edge; Redis caches fragments, sessions, and queue payloads inside the app. Use both when traffic justifies the ops overhead.

Nginx suits most Laravel and WordPress stacks when it already fronts PHP-FPM—one process, lower setup cost. Varnish wins on high-traffic anonymous HTML, ESI fragments, and fine-grained VCL logic, but adds a separate service to patch and monitor. On sister sites sharing Deployer 7 pipelines, default to Nginx unless traffic data proves Varnish is worth the ops cost.

Define a proxy_cache_path zone on fast SSD under /var/cache/nginx, point an upstream at PHP-FPM on localhost, enable proxy_cache on public GET locations with a stable proxy_cache_key, set proxy_cache_valid TTLs, and add proxy_no_cache and proxy_cache_bypass for session cookies and Authorization headers. Exclude /admin, /dashboard, /api, and /login with proxy_no_cache 1. Add X-Cache-Status always so you can debug HIT versus MISS during testing.

Set headers in middleware or controllers, not by hoping the proxy guesses. For anonymous pages, return Cache-Control: public, max-age=600, s-maxage=600 and Vary: Accept-Encoding. Use s-maxage for shared caches like Nginx and Varnish; max-age for browsers. Skip caching when a user is authenticated. Never send Set-Cookie, Cache-Control: private, or Vary: Cookie on cacheable anonymous HTML.

Ten to thirty minutes is a sensible starting range for content that changes occasionally, paired with active purge on publish.

Plan purge before launch. Options include URL purge on publish via Laravel model observers calling an internal endpoint, Varnish prefix bans, short s-maxage as a TTL fallback, and Deployer 7 or GitLab CI deploy hooks that flush edge cache after symlink swap. For Nginx, use proxy_cache_purge on localhost-only locations or delete files under the cache path on stock Ubuntu builds. Never expose unauthenticated purge URLs to the public internet.

Laravel starts sessions globally by default, so anonymous visitors often get a laravel_session cookie on the homepage. Nginx sees the cookie and bypasses cache via proxy_no_cache rules. Fix the app first: move session middleware to routes that need it, or restrict the web middleware group to authenticated sections. Blindly ignoring Set-Cookie at the proxy is dangerous because you may cache personalised HTML for everyone.

proxy_cache_key defines how Nginx stores responses. Using $request_uri includes query strings, so UTM parameters create unique keys for identical content and explode cache size. Switch to $uri when marketing params should not fragment the cache, but keep $request_uri on booking or search pages where query strings legitimately change content.

Session cookies on every response, query-string cache explosion, HTTP and HTTPS mixed in cache keys, forgetting long TTL on static assets, and no monitoring of X-Cache-Status hit rates. Rates below 60% on anonymous pages usually mean misconfigured bypass rules, not missing micro-optimisations. Hard refresh in Chrome bypasses browser cache but not always the proxy—test with curl -I and a clean URL.

Product and public pages can cache while cart, checkout, and my-account routes pass through. WooCommerce sets cookies early, so exclude those URL prefixes explicitly in Nginx location blocks. WordPress admin bar cookies cause the same problem—disable the bar for anonymous users or bypass cache when the cookie exists. There is no magic directive that fixes bad session hygiene; tune routing and headers together.

Write VCL 4.1 with a backend pointing at your origin on localhost, pass non-GET/HEAD requests, pass admin, dashboard, api, and login paths, pass requests with session cookies, unset cookies on cacheable pages, and set beresp.ttl from Cache-Control or a default like 10 minutes for 200 responses. Varnish strips cookies at the edge, so your origin must not depend on cookies for anonymous HTML. Terminate TLS in front with Nginx or hitch because Varnish needs a separate TLS layer.

Yes. Terminate SSL at Nginx or Varnish, talk HTTP to localhost upstream only, and keep HTTPS out of cache keys. If health checks hit HTTP while users hit HTTPS, you duplicate cache entries and waste disk. The proxy handles SSL termination, caching rules, and forwarding uncacheable requests unchanged in one layer.

Add X-Cache-Status $upstream_cache_status always on Nginx responses and parse access logs for HIT, MISS, BYPASS, and STALE values. Hit rates below 60% on anonymous pages signal cookie leaks, wrong bypass rules, or query-string key fragmentation. Compare curl -I headers against your Cache-Control contract when debugging unexpected MISS statuses during optimization work.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: