All articles
CachingHTTPPerformanceBackendOptimization

HTTP Caching: The Free Performance Win Nobody Uses

PKParth Kanasagra
8 min read

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.js changes 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

Related Articles

PK

Parth Kanasagra

Certified Senior Laravel & PHP Developer · 9+ years experience · Available for hire