TL;DR
Every time you click a link, an invisible army of caches springs into action. From your browser to DNS servers, from frontend apps to backend databases, caching happens at every single layer. DNS caches save you from repetitive lookups. Browser caches prevent re-downloading the same logo 47 times. Backend caches like Redis give your database a break. And databases themselves? They keep hot data in memory so they don’t have to hit the disk every time someone asks for your user profile.
The trick is balancing speed with freshness. Cache-aside loads data on demand. Read-through makes caching transparent. Write-through keeps cache and database in sync. Write-behind sacrifices consistency for blazing fast writes. Each strategy has trade-offs, and picking the right one can make your app feel instant instead of sluggish.
🔥 Introduction
You know that feeling when you revisit a website and it loads instantly? Like, faster than you can blink? That’s not magic. That’s caching doing its job at five different layers simultaneously, all working together like a relay team passing the baton.
When you click a link or type a URL, your request doesn’t just teleport to a server and back. It goes on a journey. And at every stop along the way, there’s a cache waiting to either hand you the answer immediately or pass you along to the next layer. It’s beautiful, really. And most people have no idea it’s happening.
Let me walk you through it. We’re going to follow a single web request from your browser all the way to the database and back, meeting every cache along the way. By the end, you’ll see why caching isn’t just „a performance trick.“ It’s the foundation of how the modern web actually works.
DNS Resolution: Finding the Server Faster
Before your browser can even think about fetching a webpage, it needs to know where to send the request. That means turning example.com into an IP address. And that’s where DNS comes in.
Here’s the thing: DNS lookups can be slow. Really slow. Your computer has to ask a recursive resolver, which might ask multiple authoritative servers scattered across the internet. It’s like asking for directions by playing a game of telephone across three continents.
But we don’t do that every time. Because DNS caching exists.
The first time you visit a site, your system does the full lookup. But it also stores that IP address locally for a while (based on the domain’s TTL, or Time to Live). So when you visit example.com again five minutes later, or when some embedded image on the page needs to connect to cdn.example.com, your system just checks its local address book and moves on. No extra round trips. No wasted time.
Your OS caches DNS. Your router caches DNS. Your ISP’s DNS server caches DNS. It’s caches all the way down.
Sure, these caches don’t last forever. TTLs are usually short, just a few minutes or hours. That way, if the site moves to a new server, you’ll eventually pick up the change. But within that window? Instant lookups. And that saves so much time on repeat visits.
It’s the first layer of caching in our journey, and it happens before you even hit the network.
Browser Cache: Your First Line of Defense
Okay, now your browser knows the server’s IP. Time to fetch the page, right?
Not so fast. The browser checks its own cache first.
Think about it. That logo at the top of the page? You’ve seen it before. The CSS file that styles the whole site? You downloaded it yesterday. Why would you fetch them again?
You wouldn’t. And the browser knows that.
Every HTTP request your browser makes goes through the cache layer first. If the browser has a fresh copy of the resource, it just grabs it from disk (or memory) and uses it. No network call. No latency. Just instant retrieval.
This is governed by HTTP headers like Cache-Control, ETag, and Last-Modified. The server tells your browser how long it can trust a resource, and the browser respects that. If the cached version is still fresh, it uses it. If it might be stale, the browser sends a quick conditional request to the server asking, „Hey, did this change?“ If the server says no, the browser reuses its cached copy. If it says yes, the browser downloads the new version.
This is why returning to a website feels faster than visiting it for the first time. Most of the heavy assets are already sitting on your machine.
But here’s the catch: developers have to design their assets with caching in mind. If you name your files style.css, the browser might keep serving an old version even after you update it. That’s why you see filenames like style.v2.css or bundle.a1b2c3d4.js. Those hashes force the browser to treat them as new files when they change. It’s called cache busting, and it’s the dance we do to keep caches useful without serving stale content.
Frontend Application Cache: Speedy Repeat Visits and Offline Tricks
Modern web apps don’t stop at the browser’s built-in cache. They often add their own caching layer on top.
Let’s say you’re using a single-page app like Gmail or Trello. The first time you load your inbox, the app fetches your messages from the server. But it also stores them in memory (or in LocalStorage or IndexedDB). So when you click away and come back, the app doesn’t make another API call. It just shows you the cached messages instantly.
This is frontend caching in action. The app is managing its own little data store, deciding what to keep around and when to refresh it.
Service workers take this even further. They’re background scripts that intercept network requests and can serve cached responses on the fly. This enables offline-first apps. You visit a site once, and the service worker caches the app shell and key assets. Next time, even if you’re offline, the app loads. It might show you stale data, but it loads. And when you’re back online, the service worker fetches fresh data in the background and updates the cache.
This is how Progressive Web Apps (PWAs) work. They feel fast because they’re serving everything from cache first, then quietly updating in the background. It’s a „cache-first, network-second“ strategy, and it’s incredibly effective for user experience.
But it also means the app code is responsible for cache invalidation. If you cache a user’s profile, when do you refresh it? After a certain time? When they pull-to-refresh? When they log back in? These are the questions frontend developers wrestle with, because getting caching wrong means showing users outdated information.
Still, when done right, frontend caching makes apps feel instant. And that’s worth the complexity.
Backend Distributed Cache: Lightning-Fast Data on the Server
Alright, your request made it past the browser and frontend. Now it’s hitting the backend server.
And guess what? There’s a cache here too.
When your backend receives a request, one of the first things it does is check an in-memory cache like Redis or Memcached. These are distributed caches that live in RAM, and they’re insanely fast. We’re talking microsecond response times.
Here’s how it works. Let’s say your request is asking for „the top 10 most popular products.“ The first time someone asks for that, the backend queries the database, calculates the ranking, and returns it. But before sending the response, it also stores the result in Redis with a key like top_products.
Next time someone asks for the same thing, the backend checks Redis first. If the key exists, it grabs the cached result and returns it immediately. No database query. No recalculation. Just a lightning-fast lookup.
This pattern is called cache-aside (or lazy loading). The cache sits „on the side“ of the database, and the app is responsible for populating it on cache misses.
The impact is huge. Instead of every user hitting the database for the same popular query, only the first user pays the cost. Everyone else gets the cached version. This reduces database load, speeds up response times, and lets your system handle way more traffic with the same hardware.
Because these caches are distributed, multiple backend servers can share the same cache. That means if Server A populates the cache, Server B can use that cached data too. It’s a shared memory pool across your entire backend fleet.
Of course, cache invalidation is the hard part. When the underlying data changes (like a product’s price updates), you need to either invalidate the cache entry or let it expire naturally via TTL. Some teams explicitly delete cache keys on writes. Others just set short TTLs and accept that data might be slightly stale for a few seconds.
It’s a trade-off. But for read-heavy workloads, backend caching is basically mandatory. Without it, your database would get hammered into the ground.
Database Cache: Speeding Up the Source of Truth
Even when the backend cache misses and you have to hit the database, caching still isn’t done.
Databases themselves cache aggressively. They have to. Reading from disk is slow compared to reading from RAM, so databases keep recently accessed data in memory buffers.
Take MySQL’s InnoDB engine. It has a buffer pool that stores recently read disk pages in memory. So if your backend queries for a user’s profile, and then queries for it again a few seconds later, the database doesn’t hit the disk twice. It serves the second read straight from its buffer cache.
The same goes for query plans. Databases cache the execution plans for queries so they don’t have to recompute them every time.
Some databases used to have explicit query result caches (MySQL had one, but it became a bottleneck and got removed in newer versions). These days, most systems handle query result caching at the application layer with Redis or similar tools. But the principle is the same: avoid doing expensive work twice.
Even with all the caching happening in the application and distributed cache layers, the database’s own memory caching is still critical. It’s the last line of defense before hitting slow disk I/O. And in high-traffic systems, it can make the difference between handling 1,000 requests per second and 10,000.
Caching Strategies: Cache-Aside, Read-Through, Write-Through, Write-Behind
So we’ve seen where caching happens. But how you cache matters just as much.
There are a few common patterns, each with different trade-offs. Let’s break them down.
Cache-Aside (Lazy Loading)
This is the simplest and most common pattern. The cache sits „on the side,“ and the app manages it.
On a read, the app checks the cache. If there’s a hit, great. If not, it reads from the database, stores the result in the cache, and returns it.
On a write, the app updates the database. It might also invalidate or update the cache, but that’s optional.
Pros: Simple. You only cache what’s actually needed. The cache can go down and the system still works (just slower).
Cons: The first request after a cache miss is slow. And if you don’t invalidate the cache on writes, you can serve stale data.
This is what most teams use by default. It’s easy to reason about and works well for read-heavy workloads.
Read-Through Cache
Similar to cache-aside, but the cache itself handles the database lookup on a miss. From the app’s perspective, it just reads from the cache, and the cache transparently fetches from the database when needed.
Pros: Cleaner app code. The cache layer abstracts the complexity.
Cons: Still has the stale data problem. And the first read for any key is slow.
This is common with caching libraries that provide a unified interface, like AWS’s DynamoDB Accelerator (DAX).
Write-Through Cache
Every write goes through the cache and the database. The cache stays in sync with the database automatically.
Pros: No stale data. After a write, subsequent reads get the latest version from the cache.
Cons: Writes are slower because you’re doing two operations (cache + database). And if the cache goes down, writes might fail unless you have fallback logic.
This is great for use cases where data consistency is critical, like inventory counts in e-commerce.
Write-Behind (Write-Back) Cache
Writes go to the cache only, and the cache asynchronously flushes them to the database later.
Pros: Writes are fast. The app just writes to memory and moves on. The cache handles the database write in the background, often batching multiple writes together.
Cons: If the cache crashes before flushing, you lose data. And the database can be temporarily out of sync, which is risky if other services read directly from it.
This is used in write-heavy scenarios where you can tolerate eventual consistency, like analytics logging or user activity tracking.
Each of these patterns can be mixed and matched. You might use read-through for reads and write-through for writes. Or cache-aside with aggressive TTLs. The right choice depends on your workload, consistency requirements, and tolerance for complexity.
But the core idea is the same: cache the stuff you use a lot, and figure out how to keep it reasonably fresh.
🎯 Conclusion
So there you have it. The life of a web request, from browser to backend and back, with caches at every single layer.
DNS caches eliminate repetitive lookups. Browser caches prevent redundant downloads. Frontend apps cache data for instant navigation. Backend caches like Redis shield the database from being obliterated. And databases themselves keep hot data in memory to avoid slow disk reads.
It’s a relay race, and caching is the baton being passed at every leg. Without it, the web would grind to a halt.
Sure, caching introduces complexity. Keeping caches fresh is hard. Cache invalidation is famously one of the two hardest problems in computer science (along with naming things). But the performance gains are so massive that it’s worth it. A well-cached system can handle 10x or 100x more traffic than an uncached one.
Next time you load a page and it feels instant, remember: there’s an invisible army of caches working together to make that happen. And somewhere, a developer is losing sleep over whether to set a TTL of 60 seconds or 300.
Such is life.