HTTP Caching: The Free Performance Win Nobody Uses
I ran a performance audit on a client API last year. Their product listing endpoint did about 50ms of database work and returned data that changed once a day. They were receiving 50,000 requests a day to that endpoint. No cache headers. Every client, every single request, hitting the database for the same result.
We added two header lines and cut database load by 60%. HTTP caching is one of the most impactful and most ignored performance tools in backend development.
How It Works
When you set Cache-Control headers, you tell the client (browser, mobile app) and any intermediary (CDN, proxy) how long they can reuse the response without hitting your server again:
Cache-Control: public, max-age=3600
This tells any cache: "this response is public and valid for 3,600 seconds." The next request in that window never reaches your server.
Cache-Control Directives That Matter
# Public — CDN + browser can cache
Cache-Control: public, max-age=86400 # 24 hours
Cache-Control: public, max-age=31536000, immutable # 1 year for versioned assets
# Private — browser only, not CDN
Cache-Control: private, max-age=600 # User-specific data
# No caching
Cache-Control: no-store # Never store (sensitive data)
Cache-Control: no-cache # Store but always revalidate
ETags: Conditional Requests
// Send with response
Cache-Control: public, max-age=60
ETag: "abc123"
// Client resends after expiry
If-None-Match: "abc123"
// If unchanged — 304 Not Modified, no body
// Saves bandwidth even when you cannot save the server round-trip
In Laravel:
public function index(): JsonResponse
{
$products = Cache::remember('products', 3600, fn() => Product::all());
$etag = md5($products->updated_at?->max() . $products->count());
return response()
->json(['data' => $products])
->withHeaders(['Cache-Control' => 'public, max-age=300'])
->setEtag($etag);
}
CDN Caching
A CDN (Cloudflare, CloudFront, Fastly) caches your responses at edge nodes around the world. When you set Cache-Control: public, the CDN serves most requests without ever reaching your origin server. For a typical PHP application serving public content, a CDN can handle 80-90% of traffic.
What to Cache and What Not to Cache
GET /api/products → Cache-Control: public, max-age=3600 ✓
GET /blog/posts → Cache-Control: public, max-age=1800 ✓
GET /api/user/orders → Cache-Control: private, max-age=300 ✓ (user-specific)
POST /api/payments → Cache-Control: no-store ✓ (mutations)
GET /api/notifications → Cache-Control: no-store ✓ (real-time)
Cache Invalidation
TTL-based expiry is enough for most applications — accept a few minutes of staleness and users will not notice. For content that must be fresh immediately after updates, use one of:
- Versioned URLs —
/assets/main.a3b4c5.jschanges when the file changes - CDN purge API — when a product is updated, purge
/api/products/{id}via the CDN API - Surrogate keys — tag responses, purge all responses with that tag on update