Caching
By the Syncfusion DevTutorials team · Last updated
Keep popular data somewhere fast: hits and misses, why small caches work, cache-aside, write strategies, TTLs, LRU and LFU eviction, cache stampedes and what not to cache.
Think about a busy restaurant kitchen.
The chef keeps frequently used ingredients such as salt, oil, onions, and chilli within easy reach on the counter. Rarely used ingredients are stored in the storeroom at the back. Walking to the storeroom takes time, while reaching for salt takes only a second. Because the chef repeatedly uses the same ingredients throughout the day, most tasks are completed quickly.
A cache works in a similar way. It keeps a small collection of frequently requested data in a very fast location so that most requests avoid the slower trip to the database.
The main idea: store copies of popular data in a fast location so that most requests do not need to access the database.
What You Will Learn
In this lesson, you will learn:
- What a cache is and what hits, misses, and hit ratio mean.
- Why a small cache can serve most requests.
- Where caches exist throughout a system.
- How the cache-aside pattern works.
- Three common ways to handle writes.
- How expiration (TTL) works and the trade-offs involved.
- How eviction policies such as LRU and LFU work.
- What cache stampedes are and how to prevent them.
- Which types of data should not be cached.
A cache read takes about 1 ms and a database read about 30 ms (Lesson 3). QuickBite gets about 1,157 menu views per second at lunch, with 20 reads for every write (Lesson 2), so caching is a great fit.
Hits, Misses and the Hit Ratio
Two Possible Outcomes
Whenever an application checks the cache, one of two things happens:
- Hit: the requested data is already in the cache. The response is fast.
- Miss: the data is not in the cache, so the application must retrieve it from the database.
The hit ratio is the percentage of requests served from the cache.
For example, a hit ratio of 90% means that 9 out of every 10 requests are served directly from the cache.
Why the Hit Ratio Matters
The chart shows the average read time for four hit ratios. A cache read takes 1 ms and a database read takes 30 ms.
Going from 0% to 95% hits cuts the average read from 30.0 ms to 2.5 ms: 12 times faster.
| Hit ratio | Average read time |
|---|---|
| 0% | 30.0 ms |
| 50% | 15.5 ms |
| 80% | 6.8 ms |
| 95% | 2.5 ms |
A higher hit ratio provides two important benefits:
- Requests become significantly faster.
- The database receives fewer requests, reducing load and improving scalability.
Why a Small Cache Works
A Few Items Receive Most Traffic
QuickBite may have thousands of restaurants, but customers do not view each restaurant equally. A small number of popular restaurants receive a large percentage of all traffic.
This pattern appears throughout software systems and is often described using the 80/20 rule, where a small percentage of data receives most of the requests.
Suppose QuickBite has 1,000 menus, and menu number k is opened about 1/k as often as menu number 1. The chart shows how much of all traffic the most popular menus get.
The top 10 menus (1% of all) get 39.1% of views. The top 100 menus (10% of all) get 69.3% of views.
By storing only the most popular menus in memory, the cache can satisfy most requests without storing every menu.
This is why even relatively small caches can provide substantial performance improvements.
Where Caches Live
Caches exist throughout the request path.
| Where | What it keeps | QuickBite example |
|---|---|---|
| Phone or browser | Previously downloaded files | QuickBite logo |
| CDN | Files stored close to users | Menu images |
| Application server memory | Frequently used local data | List of cities |
| Shared cache (Redis, Memcached) | Shared data used by all servers | Menus, ratings, prices |
| Database memory | Recently accessed rows | Managed internally by the database |
This lesson focuses on the shared cache, which is a fast in-memory storage system available to all application servers.
Popular examples include Redis and Memcached.
The Cache-Aside Pattern
Reading Data
The most common caching strategy is cache-aside.
In this pattern, the application manages the cache itself.
- Check the cache first.
- If the data is missing, read it from the database.
- Save a copy in the cache for future requests.
Step through five menu views (menus 42, 42, 7, 42, 7) and watch the database read count:
The code behind the animation:
Five menu views, only two database reads
const cache = new Map();
let databaseReads = 0;
function readMenuFromDatabase(id) {
databaseReads++;
return `menu #${id}`;
}
function getMenu(id) {
if (cache.has(id)) return cache.get(id); // 1. hit: done
const menu = readMenuFromDatabase(id); // 2. miss: go to the database
cache.set(id, menu); // 3. keep a copy for next time
return menu;
}
for (const id of [42, 42, 7, 42, 7]) getMenu(id);
console.log(`Menu views: 5, database reads: ${databaseReads}`);
Expected output:
Menu views: 5, database reads: 2
Although there were five menu requests, the database was accessed only twice because the cache handled the remaining requests.
Writing Data
When information changes in the database, cached copies may become outdated.
For example, if a restaurant changes a menu price, customers should not continue seeing the old value.
The wrong way first: forget the cache
Step through a price change. First the app updates the database and forgets the cache. Then it does it the right way:
In the wrong way, the database says 280 but the customer sees 250. The database has the correct value, but the cache still has an old copy.
The right way: update the database, then remove the cache entry
The last two steps of the animation show the fix. The app updates the database, then deletes the old copy. The next read misses, fetches the new price and keeps it. The customer sees 280.
The order is extremely important:
- Update the database first.
- Remove the cached copy afterward.
If the cache entry is removed first, another request could read the old value from the database and place stale data back into the cache.
Here is the same idea in code:
The right way: delete the old copy so the next read fetches the new price
const database = { biryani: 250 };
const cache = new Map([["biryani", 250]]);
function getPrice(item) {
if (cache.has(item)) return cache.get(item);
const price = database[item];
cache.set(item, price);
return price;
}
function setPrice(item, price) {
database[item] = price; // 1. update the database first
cache.delete(item); // 2. then remove the old copy
}
setPrice("biryani", 280);
console.log(`Customer sees: ${getPrice("biryani")}`);
Expected output:
Customer sees: 280
Three Ways to Handle Writes
Cache-Aside
Write to the database and then remove the cached copy. The next read request reloads the data into the cache.
This is the simplest and most commonly used approach.
Write-Through
Write to both the cache and the database at the same time.
Advantages:
- Cache always contains fresh data.
Disadvantages:
- Every write takes slightly longer.
Write-Behind
Write to the cache immediately and save to the database later.
Advantages:
- Very fast writes.
Disadvantages:
- Data can be lost if the cache fails before the database update occurs.
For QuickBite, cache-aside is usually the best default choice. Systems handling orders and payments should avoid write-behind because data loss is unacceptable.
Expiry: How Long Should Data Stay in the Cache?
Time To Live (TTL)
Even with careful invalidation, cached data can become outdated.
For this reason, cache entries often have a Time To Live (TTL).
After the TTL expires, the entry is automatically removed and fetched again when needed.
Choosing a TTL involves a trade-off:
- Short TTL: fresher data but more database reads.
- Long TTL: higher hit ratio but older data.
Different types of data deserve different TTL values.
| QuickBite data | Sensible TTL | Why |
|---|---|---|
| Cities list | 1 day | Changes very rarely |
| Restaurant menus | 5 minutes | Changes occasionally |
| Ratings | 30 seconds | Slight staleness is acceptable |
| Order status | Do not cache | Must always be accurate |
When the Cache Is Full
Caches are intentionally limited in size.
When the cache becomes full, some existing entry must be removed to make room for new data.
The rule used to decide which item is removed is called an eviction policy.
LRU (Least Recently Used)
LRU removes the item that has not been accessed for the longest time.
Think of a shelf where recently used items stay at the front while unused items slowly move toward the back.
Step through six opens on a cache that holds 3 menus: Pizza, Dosa, Biryani, Pizza, Burger, Dosa.
The code behind the animation:
An LRU cache of 3 menus: the one used longest ago is thrown out
const capacity = 3;
const cache = new Map(); // a Map remembers the order items were added
function open(menu) {
if (cache.has(menu)) {
cache.delete(menu); // move it to the "most recent" end
cache.set(menu, true);
return "hit";
}
if (cache.size === capacity) {
const oldest = cache.keys().next().value; // the first key is the least recent
cache.delete(oldest);
}
cache.set(menu, true);
return "miss";
}
const opens = ["Pizza", "Dosa", "Biryani", "Pizza", "Burger", "Dosa"];
const results = opens.map((m) => `${m}:${open(m)}`);
console.log(results.join(" "));
console.log(`In the cache now: ${[...cache.keys()].join(", ")}`);
Expected output:
Pizza:miss Dosa:miss Biryani:miss Pizza:hit Burger:miss Dosa:miss
In the cache now: Pizza, Burger, Dosa
The result is 1 hit and 5 misses. Dosa had not been used recently, so it was removed when space was needed.
LFU (Least Frequently Used)
LFU removes the item that has been accessed the fewest number of times.
Unlike LRU, LFU focuses on long-term popularity rather than recent activity.
This becomes useful when a temporary process, such as a nightly report, reads many items only once.
In that situation:
- LRU may remove popular items.
- LFU keeps frequently used items because they have accumulated many accesses over time.
Try It: Size the Cache
Adjust the cache size and compare LRU with LFU.
Notice:
- Even a cache containing only a small percentage of all menus can achieve a high hit ratio.
- Increasing cache size usually provides diminishing returns. Doubling the cache rarely doubles performance.
Cache Stampedes
What Goes Wrong
Imagine the most popular menu receives 200 requests every second.
If its cache entry expires, many requests may miss at exactly the same time. Every request immediately falls back to the database.
This sudden surge of identical requests is called a cache stampede or thundering herd problem.
Step through what happens when the most popular menu expires. Rebuilding it from the database takes 50 ms:
Without a lock, that is 10 identical database reads. With a lock, it is 1 database read, and the rest wait a moment.
One popular item may not create much load by itself. However, if hundreds of popular items expire simultaneously, the database can be overwhelmed.
Two Common Solutions
Allow Only One Request to Rebuild
The first request that notices the missing entry rebuilds it while the others briefly wait.
This prevents duplicate database work.
Add TTL Jitter
Instead of giving every entry exactly the same expiration time, add a small amount of randomness.
For example:
- 295 seconds
- 301 seconds
- 307 seconds
This spreads expirations over time and prevents large groups of entries from expiring simultaneously.
What Should Not Be Cached
Caching works best for frequently read data that can tolerate slight staleness.
Some types of information should not be cached.
Examples include:
- Payment status.
- Order status.
- Inventory that is nearly sold out.
- Sensitive user information shared incorrectly between users.
Caching is also less useful when requests rarely repeat. If almost every request asks for different data, the hit ratio will remain low and the cache provides little benefit.
A cache is only a copy of the data. The database remains the source of truth. If the application cannot function correctly when the cache is empty, then the cache has become more important than it should be.
Easy way to remember: check the cache first, fall back to the database if needed, then store a copy. When data changes, update the database first and remove the cached copy afterward.
Common Mistakes
| Mistake | Wrong | Correct | Why it happens |
|---|---|---|---|
| Forgetting to remove old copies | Update the price in the database and stop there. | Update the database, then delete the cache entry. | Otherwise customers keep seeing the old price until the TTL runs out. |
| Deleting in the wrong order | Delete the cache entry, then update the database. | Update the database first, then delete the cache entry. | In the gap between the two steps, another request can read the old value and put it back in the cache. |
| No TTL | Cache items forever. | Give each kind of data a TTL that matches how often it changes. | Without expiry, any copy you forget to delete stays wrong forever. |
| Everything expires at once | Every menu gets a TTL of exactly 300 seconds. | Use 300 seconds plus or minus a few, and let one request rebuild each hot item. | Identical TTLs line up expiries and send a stampede to the database. |
| Treating the cache as the truth | Saving orders only in the cache, because it is fast. | The database holds the truth. The cache only holds copies. | Caches can restart and lose everything. That must never lose an order. |
Practice
What to Remember
- A cache stores a copy of frequently used data in a fast location.
- The hit ratio is the most important caching metric.
- Even a small cache can serve most requests because traffic is usually uneven.
- Cache-aside is the most common caching strategy.
- When data changes, update the database first and then remove the cached copy.
- Every cache entry should have an appropriate TTL.
- LRU removes the least recently used item, while LFU removes the least frequently used item.
- Prevent cache stampedes using locks and TTL jitter.
- Never cache data that must always be perfectly accurate, such as payment status.
Further Reading
These are the sources this lesson was checked against:
- Microsoft Azure Architecture Center: Cache-Aside Pattern: the read and write steps, why the order matters, expiry, eviction and when not to use it.
- System Design Primer: Cache: where caches live, cache-aside, write-through and write-behind.
- ByteByteGo: Back-of-the-Envelope Estimation: estimating cache size alongside QPS and storage.
Next: Data Storage and Replication, where you will learn how QuickBite stores menus, orders, and rider information, and why copies of a database may not always contain the latest data.