Architecture

Application Caching: Should You Add a Cache When Your App Is Slow?

Application caching only helps when work repeats. Learn when to add a cache, how browser, CDN, app and database caches differ, and what they cost.

Aarav Sharma

Aarav Sharma

October 31, 20267 min read
Share
Choosing between fixing a slow query and adding a cache in front of it

Your API used to respond almost instantly. Now pages hang, database queries get slower every week, and someone on the team suggests the obvious fix: put Redis in front of it.

Maybe that's the right call. Application caching saves you from doing the same work twice, but it does nothing for a query that's missing an index, a loop that processes far more data than it needs, or a downstream service that's slow on its own. Cache the wrong thing and you've added a moving part without touching the real problem.

So before you pick a cache, find out where the time goes. This post covers how to tell whether caching fits, which layer to put it in, and what it costs you once it's there.

Quick Answer

Add caching when your application keeps fetching the same data or repeating the same expensive work, and the result is safe to reuse for a while. A cache can cut response times, database load and repeated computation. It won't fix an inefficient query or unnecessary processing, so fix those first. Then pick the caching layer based on where the repeated work happens and how fresh the data has to be.

What Is Caching?

Picture a restaurant where the staff walk back to the office every time a customer asks for a menu. Eventually someone leaves a stack of menus at the counter. That's caching: you keep a copy of something close by so you don't go back to the source every time.

In an application, the copy might be a database row, an API response or the result of a slow calculation. Say your online store loads the same product categories every time someone opens the homepage. If those categories change once a month, you're running the same query over and over for nothing.

Two terms come up constantly:

  • Cache hit: the data is in the cache, so your application reuses it.
  • Cache miss: the data isn't there, so your application goes back to the original source, usually the database, and does the full work.
A request checks the cache first, returns the stored copy on a hit, and goes to the database and stores the result on a miss

A cache doesn't replace your database, and it doesn't speed up every request. It only helps when a result gets reused.

The Problem

The hard part is knowing which work to avoid. Imagine an endpoint that runs four queries to build one response. If every request runs the same four queries with the same inputs, caching the final response takes real load off the database.

Now imagine the endpoint is slow because one of those queries scans the entire orders table for lack of an index. A cache hides that on a hit, but every miss still pays the full price. After a deploy, a restart or an eviction, you're back on the slow path, often at the worst possible moment.

There's a second problem: cached data goes out of date. Suppose a product's price drops from ₹1,000 to ₹900 and the cache still holds the old value. Your page loads fast and shows the wrong price.

So caching is more than storing data somewhere. You have to decide what's safe to reuse, how long to keep it, and what happens when the original changes.

The Solution: Choosing the Right Application Caching Layer

Start by measuring. Look at your slowest endpoints, check which queries they run, and see whether the same requests keep arriving with the same inputs. If one query dominates, run it through your database's EXPLAIN before you reach for a cache.

Once you know the bottleneck, pick the layer that removes it. A cache can sit in four places along a request's path, and each one skips a different piece of work.

A request passes through the browser cache, the CDN, the application cache and the database cache, and each layer can answer before the next one is reached

Browser caching

The browser can keep images, stylesheets and JavaScript files and reuse them on the next visit instead of downloading them again. Returning users make fewer network requests and see pages load faster.

You control this mostly through HTTP headers such as Cache-Control. To see what a site sends today, check its response headers:

curl -sI https://developer.mozilla.org/en-US/ | grep -i -E '^(cache-control|etag|age):'

A long max-age suits a fingerprinted file like app.3f9a1c.js, because a new build gets a new filename. It's the wrong choice for your HTML, which has to pick up changes on the next visit. The MDN guide to HTTP caching covers the full set of directives.

CDN caching

A content delivery network (CDN) keeps copies of your content in locations closer to your users. A request from Pune can be answered nearby instead of travelling all the way back to your origin server.

This works well for images, scripts and anything else that's the same for every visitor. Be careful with personalised responses. If your cache rules don't separate users, one person's account page can end up in front of someone else. Cloudflare's caching overview explains what a CDN caches by default and how to change it.

Application caching

Here your backend keeps frequently requested data or expensive results, either in process memory or in a shared store like Redis. If many requests ask for the same product, your application answers from the cache and queries the database once.

The most common pattern is cache-aside: check the cache, fall back to the database on a miss, then store the result. Here's a minimal version you can run with Node.js:

// cache-aside.js: run with `node cache-aside.js`
const TTL_MS = 60_000
const cache = new Map()

// Stand-in for your database
const db = { 42: { id: 42, name: 'Wireless mouse', price: 1000 } }

function getProductFromDb(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ ...db[id] }), 300) // simulate a slow query
  })
}

async function getProduct(id) {
  const key = `product:${id}`
  const entry = cache.get(key)
  if (entry && entry.expiresAt > Date.now()) return entry.value // cache hit

  const product = await getProductFromDb(id) // cache miss
  cache.set(key, { value: product, expiresAt: Date.now() + TTL_MS })
  return product
}

function updateProductPrice(id, price) {
  db[id].price = price
  cache.delete(`product:${id}`) // drop the stale copy
}

async function timedRequest(label) {
  const start = Date.now()
  const product = await getProduct(42)
  console.log(`${label}: ₹${product.price} in ${Date.now() - start} ms`)
}

async function main() {
  await timedRequest('request 1')
  await timedRequest('request 2')
  await timedRequest('request 3')
  updateProductPrice(42, 900)
  await timedRequest('after price update')
}

main()

The first request waits on the simulated query, and the next two come straight from memory. After the price update, the fourth request misses and returns ₹900, because updateProductPrice deleted the old entry. Leave that delete out and you'd keep serving ₹1,000 until the entry expires.

In production, you'd swap the Map for Redis or Memcached so every server shares one cache and memory doesn't grow without limit. The logic stays the same.

Database caching

Databases keep frequently read data in memory so they don't go to disk every time. PostgreSQL has shared buffers, MySQL's InnoDB has its buffer pool, and some setups add a query result cache in front.

This helps repeated reads, but it won't rescue a bad query. A full table scan served from memory is still a full table scan. How much you gain depends on your database and how you've configured it.

You don't have to pick only one of these. Most production systems use several layers together. What matters is putting the cache where the repeated work happens, not adding every layer because you can.

Trade-offs

A cache makes some requests faster, and it also gives you new ways for things to break.

  • Stale data: the price drops to ₹900, but the cache keeps returning ₹1,000 until the entry expires or something deletes it.
  • Cache misses: a request for an item that isn't cached still goes to the database. If that query is slow, the request is slow.
  • More moving parts: a user updates their profile, nothing invalidates the cached copy, and the app keeps showing the old details. Now you have a bug that only shows up some of the time.
  • Cache stampedes: a popular entry expires and many requests miss at the same moment. They all hit the database together, right when it's under the most load.
  • A misleading hit rate: your cache can report a high hit rate while users still wait, because the slowest requests are the ones that miss or depend on another service.
Caching reduces repeated work and database load, but adds stale data, slow misses, stampedes and invalidation work

Caching is the wrong tool when the data changes on almost every request, when every user sees something different, or when a value has to be exactly right at the moment you act on it. Don't approve a payment or deduct a balance based on a cached number. Read the source of truth.

For data that changes often but can tolerate a little lag, use a short expiry or delete the entry whenever you write. You don't need to solve every one of these before you add a cache. You do need to know which ones matter for your data.

Conclusion

On Monday, don't start with the Redis docs. Open your slowest endpoint, measure where the time goes, and fix any query or processing problem you find. If the same expensive work still repeats and the result is safe to reuse, add a cache at the layer where that work happens, give it an expiry, and decide how you'll invalidate it. Then measure again, and include what happens on a miss.

Contact us

Aarav Sharma

Aarav Sharma

Lead Software Engineer

Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.

Related articles

Let's Collaborate

Tell us about your project and we'll come back with a plan, a timeline, and a quote.

Project Type

Budget

Task Message

Your Contacts