Part 3 · The Building Blocks

Caching Part 1: Where to Cache and Why

Why ask the database the same question 10,000 times a second? What caches are, why they work so well, every layer you can cache at — browser to buffer pool — what to cache, and how Redis and Memcached compare.

Lesson 1510 min readbeginner#caching#redis

On this page

  1. The Problem
  2. The Core Idea
  3. How It Works
  4. Trade-offs
  5. In the Real World
  6. Interview Questions
  7. Key Takeaways
  8. Further Reading

The Problem

Your product page takes 400 ms to load. For each view, the app runs six database queries: product details, price, stock, reviews, seller info and recommendations. At 10 views per second, the database copes. At 10,000 views per second during a sale, it falls over. And most of those queries return exactly the same answer they returned one second ago.

Why ask the database the same question 10,000 times a second? That's the question caching answers. It's the single most powerful performance tool in system design.

Caching means keeping a copy of data in a place that is faster to reach, so you can reuse it instead of working it out or fetching it again. Latency is the time one request takes. A database query is one request to the database to read or change data.

The Core Idea

Think about your kitchen.

  • The things you use every day (salt, oil, tea) sit on the counter, right next to the stove.
  • Less common items are in the cupboard.
  • Bulk supplies are in the storeroom.
  • If you run out, you go to the shop.

You don't walk to the shop every time you need salt. You keep a small amount close by, because you use it often.

A cache is that counter. It's a small, fast storage layer that keeps copies of frequently used data close to where it's needed, so you don't have to go back to the slow source (the database, another service, or a distant server) every time.

Two words you'll use constantly:

  • Cache hit: the data was in the cache. Fast!
  • Cache miss: it wasn't, so we go to the source, then (usually) save the result in the cache.
  • Hit ratio: the share of requests that are hits. If 95 of 100 requests are hits, the hit ratio is 95%.

How It Works

Why caches work so well

Caching works because of how people actually use apps. Access is very uneven:

  • A small number of products, posts or videos get most of the views. Today's trending video gets millions of views; a video from 2014 gets a few.
  • The same user reloads the same pages.
  • Popular data is requested again and again within seconds.

This is often called the 80/20 rule: roughly 20% of the data gets 80% of the requests. It is a rough rule of thumb, not an exact law. Keep that 20% in a fast cache and you handle most traffic cheaply.

Hit ratio: the number that matters

~11 ms
average at 80% hits
20% of requests reach the DB
~3.5 ms
average at 95% hits
5% reach the DB
~1.5 ms
average at 99% hits
1% — five times less DB load than 95%
Hit ratio is the number that matters. Hit = 1 ms, database = 50 ms. · numbers

Say a cache hit takes 1 ms (one millisecond, a thousandth of a second) and a database query takes 50 ms. The average latency is hits × 1 ms plus misses × 50 ms:

Hit ratioAverage latencyRequests reaching the DB
0%50 ms100%
80%0.8×1 + 0.2×50 ≈ 11 ms20%
95%0.95×1 + 0.05×50 ≈ 3.5 ms5%
99%≈ 1.5 ms1%

Going from 95% to 99% cuts database load by five times. Small hit-ratio improvements have big effects.

Where can you cache?

Caches exist at every layer of a system, from the user's device to the database:

Browser / app
HTTP cache, memory — no network at all; hard to clear from the server
CDN edge
static files, public pages — near the user (lesson 14)
Reverse proxy
NGINX, Varnish — whole HTTP responses
In-process
an LRU map in app memory — nanoseconds, but one copy per server
Distributed
Redis, Memcached — shared by all servers; ~0.5 ms hop
Database
buffer pool — free, but can't save you from expensive queries at volume
You can cache at every layer between the user and the data. Higher up is faster but harder to control. · layers

1. Client cache. The browser keeps files according to HTTP headers (post 14). Mobile apps keep data in memory or on disk.

  • ✅ The fastest possible: no network at all.
  • ❌ You can't easily clear it from the server side.

2. CDN cache. Covered in post 14. Best for static files and public content.

3. Reverse-proxy cache. A reverse proxy is a server in front of your app that passes requests on. NGINX or Varnish in this role can cache whole HTTP responses. It's good for public pages that are expensive to build.

4. In-process (local) cache. A map or LRU cache inside your application's memory. LRU (least recently used) means that when the cache is full, it removes the item that was used longest ago.

  • ✅ Extremely fast (nanoseconds to microseconds), with no network hop.
  • ❌ Each server has its own copy. With 20 servers you have 20 caches, which can disagree and each warm up separately.
  • ❌ Lost on every restart or deploy, and uses the app's memory.
  • Good for: configuration, feature flags, small lookup tables, and very hot keys.

5. Distributed cache (Redis, Memcached). A separate cache server, or cluster (a group of servers that work as one), shared by all app servers.

  • ✅ One shared copy, so all servers see the same cached data.
  • ✅ Survives app restarts, and can hold much more data.
  • ❌ A network hop, typically well under a millisecond inside a data centre.
  • ❌ Another system to run, monitor and scale.
  • This is what people usually mean by "add a cache".

6. Database cache. Databases keep recently used data pages in memory (called shared buffers in PostgreSQL and the buffer pool in MySQL). That's why a query is often faster the second time. You get this for free, but it can't save you from running expensive queries at huge volume.

What should you cache?

Good candidates:

  • Read-heavy data that changes rarely: product details, user profiles, article pages, settings.
  • Expensive results: complex queries, aggregated counts ("1.2M views"), recommendation lists, rendered HTML.
  • Results from slow or rate-limited external APIs: currency exchange rates, weather, maps.
  • Session data (post 7: the record of who is logged in), for fast lookups on every request.

Poor candidates:

  • Data that changes on almost every read.
  • Data that must be perfectly up to date: an account balance at the moment of payment, or remaining stock during checkout. Read those from the source, or cache very carefully.
  • Huge objects that are rarely re-read.

Cache the object, not just the query

There are two common styles:

  • Query-level caching. The key is the SQL query (or a hash of it), and the value is the result. It's easy to add, but hard to invalidate. When a product changes, which cached queries contain it?
  • Object-level caching. The key is the thing itself, like product:991 or user:42:profile. The value is the assembled object. When product 991 changes, you know exactly which key to update or delete.

Object-level caching is usually cleaner and easier to keep correct.

Cache keys and TTLs

A cache works like a big dictionary. Each item has a key (its name) and a value (the data). Keys should be clear and structured:

Text
product:991user:42:profileuser:42:orders:page:1feed:home:region:IN

TTL (time to live) is how long an entry stays before it expires and is removed:

  • short (seconds) for fast-changing data like prices or scores,
  • long (hours or days) for data that rarely changes,
  • a TTL on everything as a safety net, so mistakes eventually fix themselves.

Redis vs Memcached

Both are in-memory key-value stores (databases that keep data in RAM and look it up by key) used as distributed caches.

RedisMemcached
Data typesStrings, lists, sets, sorted sets, hashes, streams…Simple strings/blobs
PersistenceOptional (snapshots, append-only log)None (pure cache)
Replication / clusteringBuilt inHandled by clients
Extra featuresPub/sub, Lua scripts, atomic counters, geo, streamsVery simple and fast
ThreadingMainly single-threaded command executionMulti-threaded
Typical useCache + sessions + leaderboards + rate limiting + queuesLarge, simple caching layers

Most new projects choose Redis (or a compatible alternative like Valkey) because it does much more than caching: rate limiters (which refuse clients that send too many requests), leaderboards with sorted sets, and simple queues. Memcached is still great for huge, simple cache fleets.

A simple cache in code (cache-aside)

Cache-aside means the application code manages the cache itself: it checks the cache first, and on a miss it reads the database and then fills the cache. This is the most common pattern. We'll cover all the strategies in Part 2.

JavaScript
async function getProduct(id) {  const key = `product:${id}`;  // 1. Try the cache  const cached = await redis.get(key);  if (cached) return JSON.parse(cached);          // hit  // 2. Miss: load from the database  const product = await db.products.findById(id);  // 3. Save to cache with a TTL (e.g., 10 minutes)  await redis.set(key, JSON.stringify(product), "EX", 600);  return product;}
step 1 / 8
App
Redis
Database
  1. GET product:991
  2. (nil) — miss
  3. SELECT * FROM products WHERE id = 991
  4. row
    ~50 ms
  5. SET product:991 … EX 600
  6. The next 10,000 requests in those 10 minutes…
  7. GET product:991
  8. hit
    ~1 ms, database untouched
Cache-aside, the most common pattern. The app owns the logic; the cache is just fast storage. · sequence · step through

Measure it

Always monitor:

  • hit ratio (overall and for important key groups),
  • latency (cache vs database),
  • memory usage and evictions (an eviction is when the cache removes an item to make space; many evictions mean the cache is too small),
  • database load before and after.

A cache with a 20% hit ratio adds complexity without much benefit. Find out why before adding more.

In-process cache
  • +Nanoseconds; no network hop
  • +Survives a cache-cluster outage
  • −One copy per server; copies can disagree
  • −Lost on every deploy or restart
Use whenConfig, feature flags, tiny lookup tables, very hot keys
Distributed cache (Redis)
  • +One shared copy for every server
  • +Survives app restarts; holds far more
  • −Network hop, another system to run
Use whenProduct and profile objects, sessions, rendered fragments
Local or shared? Most large systems use both — local for the very hottest keys. · compare

Trade-offs

  • ✅ Much faster reads, much less load on databases and APIs, and lower costs.
  • ✅ Absorbs traffic spikes that would otherwise take down the database.
  • ❌ Stale data. Stale means old. The cache can be out of date. You must decide how stale is acceptable.
  • ❌ More complexity: another system to run, plus invalidation logic.
  • ❌ New failure modes. If the cache goes down, can your database handle the full load? Often it can't (Part 2).
  • ❌ Memory costs money, and RAM is more expensive than disk.

When not to cache: before you've fixed obvious problems like missing indexes or N+1 queries (post 5). An N+1 problem is when code runs one query for a list and then one more query for each item. A cache on top of a slow query hides the problem, until the cache misses.

In the Real World

Facebook's memcache. Facebook built one of the largest caching systems in the world using Memcached, with many thousands of servers sitting between its web servers and MySQL databases. Its 2013 paper "Scaling Memcache at Facebook" describes how caching let the site serve billions of reads per second while protecting the databases.

Twitter and Redis. Twitter has long used Redis-style in-memory storage to hold users' home timelines as lists of tweet IDs, so loading your timeline is a fast memory read instead of a heavy database query.

Stack Overflow uses several cache layers (in-memory caches on each web server plus a shared Redis) to serve a huge audience from a small number of servers. Caching is a big part of why its hardware footprint stays small.

E-commerce sales. During big sales, the product pages for popular deals are read millions of times. Caching product details (while reading stock and prices from the source at checkout) is what keeps the database alive.

Instagram wrote about storing hundreds of millions of simple key-value mappings in Redis, and about tuning how the data is stored to save memory. It shows that at scale, even cache memory efficiency becomes an engineering problem.

Interview Questions

Q1Why does caching work so well for most web apps?

Access is highly skewed: a small fraction of items gets most requests (roughly 80/20), and the same items are requested again within seconds. Keeping that hot set in memory serves most traffic at memory speed, and the database only sees the long tail.

Q2Local in-process cache vs Redis — how do you choose?

In-process is fastest and needs no network, but every server has its own copy that can disagree and is lost on deploys. Redis is shared, larger and survives restarts, at the cost of a network hop and another system. Use local for tiny, very hot or static data; Redis for shared objects.

Q3Object-level vs query-level caching?

Query-level uses the SQL (or its hash) as the key, so it's easy to add but hard to invalidate — which cached queries include product 991? Object-level keys like product:991 map to one thing, so updates know exactly what to refresh or delete.

Q4What would you never cache, or cache very carefully?

Data that must be exact at the moment of use: a balance during payment, remaining stock at checkout, permissions right after they change. Read those from the source of truth, or use very short TTLs plus active invalidation.

Q5Your cache has a 20% hit ratio. What do you check?

Whether the keys are too specific (random query parameters, timestamps or user IDs in keys), whether TTLs are too short, whether the cache is too small and evicting (check the eviction metric), and whether the data really is re-read. A low-hit cache adds latency and complexity for little gain.

Key Takeaways

  • A cache keeps copies of frequently used data close and fast. Caches work because access is very uneven (the 80/20 rule).
  • The hit ratio matters most. Going from 95% to 99% cuts backend load five times.
  • You can cache at every layer: client, CDN, reverse proxy, in-process, distributed (Redis), and the database.
  • Prefer object-level keys (product:991), and set a TTL on everything.
  • Caching trades freshness and simplicity for speed. Fix slow queries first, and always measure.

Further Reading

  • The System Design Primer on GitHub (section "Cache")
  • The paper "Scaling Memcache at Facebook" (NSDI 2013)
  • The AWS Builders' Library article "Caching challenges and strategies"
  • The official Redis documentation
  • Designing Data-Intensive Applications by Martin Kleppmann (chapter 3, "Storage and Retrieval")

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series