Caching and CDN: Don't Compute What You Already Know
· Jerwin Arnado ·
Part ten of the full-stack series. So far we’ve made the app correct and safe. This layer makes it fast — and fast is a feature users feel before they notice anything else. The whole idea reduces to one sentence: don’t compute what you already know. If a result is expensive to produce and doesn’t change every second, store it and serve the stored copy. Caching does this on your servers; a CDN does it at the edge, near the user.
Caching is layered, top to bottom
There isn’t one cache — there’s a stack of them, each catching what the one above missed:
| Layer | Caches | Lives in |
|---|---|---|
| Browser | static assets, responses | the user’s machine |
| CDN / edge | static files, cacheable responses | servers worldwide |
| Application | query results, computed values | Redis / Memcached |
| Database | query plans, hot pages | the DB’s own buffers |
| OPcache | compiled PHP bytecode | the PHP runtime |
A request ideally gets answered as high up this stack as possible. The closer to the user it’s satisfied, the faster — and the less work every layer below has to do.
Application caching: the expensive-result store
The bread-and-butter pattern: wrap an expensive operation (a heavy aggregate query, a third-party API call) so the result is computed once and reused until it expires.
// Compute once, serve from Redis for an hour
$stats = Cache::remember("dashboard:{$user->id}", now()->addHour(), function () use ($user) {
return $user->orders() // the expensive query we don't want to repeat
->selectRaw('count(*) total, sum(amount) revenue')
->first();
});
Cache::remember checks the store, returns the hit, or runs the closure and stores it on a
miss. Back it with Redis in production — it’s in-memory (microsecond reads) and shared
across all your app servers, which the file/DB
cache drivers aren’t. Redis pulls double duty here: it’s also the backbone for sessions,
queues, and the rate limiter.
The hard part: invalidation
There are only two hard things in computer science: cache invalidation and naming things.
The joke is true. A stale cache serves wrong data, which is often worse than slow data. Two strategies that keep you sane:
- TTL (time-based): let it expire after N minutes. Simple; tolerates a window of staleness. Right for dashboards, counts, “good enough” freshness.
- Event-based (bust on write): when the underlying data changes, delete the key so the next read recomputes. Precise; correct immediately.
// When an order changes, kill the cached dashboard so it rebuilds fresh
Order::saved(fn ($order) => Cache::forget("dashboard:{$order->user_id}"));
Pick TTL when slightly-stale is fine and simplicity wins; pick event-based when correctness must be immediate. Many systems use both — short TTL as a safety net, plus busting on the writes that matter.
CDN: move bytes closer to people
A user across the world shouldn’t wait while bytes cross an ocean. A Content Delivery Network (Cloudflare, Bunny, CloudFront) caches your static content — images, CSS, JS, fonts, and the files in object storage — on servers near every user. First request fills the edge cache; the rest are local-fast.
Cache-Control: public, max-age=31536000, immutable
This works because of the hashed asset filenames from
the frontend post: app.a3f9.css can be cached forever, because when content changes the
hash changes, so the URL changes — instant invalidation without ever serving stale CSS. A CDN
also slashes your egress bill and absorbs traffic spikes
before they reach your origin.
Caveats and best practices
- Never cache user-private data in a shared cache without keying it per user. A per-user dashboard cached under a global key leaks one user’s data to all — an access-control bug wearing a performance hat.
- Cache the expensive thing, not everything. A 1ms query behind a cache adds complexity for nothing. Measure first; cache the proven-slow paths.
- Set a sane default TTL. An eternal cache with no expiry is a future debugging session where “the data won’t update” and nobody remembers the cache.
- A cache is an optimization, not a source of truth. The app must work correctly (just slower) with the cache flushed. If flushing it breaks correctness, you’ve built a bug.
Conclusion
Idea → don't compute what you already know
Layers → browser → CDN → app (Redis) → DB → OPcache
App cache→ Cache::remember(key, ttl, fn) on proven-slow paths
Invalidate→ TTL (simple) vs event-based bust (precise) — often both
CDN → static + files at the edge; hashed names = free invalidation
Caching and a CDN are how a correct app becomes a fast one — by doing expensive work once and serving it from as close to the user as possible. Just respect invalidation, and never cache one user’s data where another can read it. Next: load balancing and scaling — what happens when even a fast app has more users than one server can hold.