
September 11, 2026
13 min read
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.
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.
| Criterion | Nginx proxy_cache | Varnish |
|---|---|---|
| Setup complexity | Low if Nginx is already fronting PHP-FPM | Medium — separate VCL workflow |
| Cache logic | Directive-based, good for common cases | VCL scripting, very flexible |
| TLS termination | Native, well documented | Needs hitch, HAProxy, or Nginx in front |
| Purge API | proxy_cache_purge module or cache path delete | Built-in PURGE and ban/lurker |
| Best fit | Laravel 12/13, WordPress 7.1, mixed static + API | High-traffic HTML, aggressive edge caching |
| Ops footprint | One process many sites already use | Extra 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.
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.
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-Cookieon anonymous HTML — poisons the cache key or forces bypassCache-Control: private— tells shared caches to skip storageVary: 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.
- URL purge on publish — Laravel model observers call an internal purge endpoint when content changes.
- Prefix ban (Varnish) —
ban("req.url ~ ^/blog/")clears an entire section. - TTL fallback — short
s-maxagelimits staleness when purge fails. - 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.
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=…andVary: Accept-Encodingin 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-Statushit 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
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.

