mosaddik.dev
All posts

Caching explained: where to cache, for how long, and how to keep it fresh

Mosaddik Billah 5 min read

Software Engineering

A smartphone running an internet speed test
Photo by Đức Trịnh on Unsplash
On this page
  1. The layers, from closest to furthest
  2. Browser and CDN caching: the Cache-Control header
  3. Application caching: cache-aside
  4. The hard part: keeping it fresh
  5. Common mistakes
  6. What to cache first

A cache keeps a copy of something that was slow to get, close to where it’s needed, so the next request is fast. It’s the same reason you keep rice in a jar in the kitchen instead of going to the shop each time you cook. Caching is often the single biggest speed improvement you can make to a site or API, and also a common source of “why am I seeing old data?” bugs. This guide covers both.

The layers, from closest to furthest#

A request for a web page can be answered at several places, each faster than the next one along:

  1. The browser cache, on the visitor’s own device. Instant, with no network at all.
  2. A CDN (content delivery network), servers around the world that keep copies near visitors. Tens of milliseconds instead of hundreds.
  3. An application cache in or beside your server, in memory or in Redis. Skips the slow work: the database query, the external API call.
  4. The database’s own cache, which keeps frequently used data in memory. Mostly automatic.

The rule: cache as close to the visitor as the data allows. Public, rarely changing files (images, CSS, JavaScript) can sit in the browser for a year. A logged-in user’s cart can’t sit in a shared CDN at all.

Browser and CDN caching: the Cache-Control header#

Your server tells browsers and CDNs what to do with a response through the Cache-Control header:

Header Means Use for
public, max-age=31536000, immutable Keep for a year; it never changes Files with a version or hash in the name, like app.3f9a1c.js
public, max-age=300 Keep for 5 minutes Public pages or API responses that can be a little stale
public, s-maxage=60, stale-while-revalidate=600 CDN keeps for a minute, then may serve the old copy while fetching a new one Busy public pages where speed matters more than second-by-second freshness
no-cache Store it, but check with the server before each use HTML that must always be current
private, no-store Don’t store it anywhere Account pages, anything with personal data

Note that no-cache doesn’t mean “don’t cache”; no-store does.

The trick for long cache times: change the file name when the content changes. Build tools add a hash to file names automatically, so a new version gets a new URL and the long-cached old one is simply never asked for again. This site uses the same rule for blog cover photos.

Application caching: cache-aside#

The most common pattern in code is cache-aside: look in the cache, and on a miss, do the slow work and save the result.

JavaScript
const cache = new Map();

async function cached(key, ttlSeconds, load) {
  const hit = cache.get(key);
  if (hit && hit.expires > Date.now()) return hit.value;

  const value = await load();
  cache.set(key, { value, expires: Date.now() + ttlSeconds * 1000 });
  return value;
}

// The slow call now runs at most once a minute, however many visitors ask.
app.get("/products", async (req, res) => {
  const products = await cached("products", 60, () => db.getProducts());
  res.json(products);
});

ttlSeconds is the time to live: how long a cached value may be used before it’s fetched again.

An in-memory Map is fine for one server and small data, but it’s lost on restart, isn’t shared between servers, and grows without limit unless you cap it. In production, use Redis or Memcached for shared caches, or an LRU library such as lru-cache for in-process ones.

The hard part: keeping it fresh#

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

A cache is a copy, and copies go out of date. There are two main ways to deal with that:

  • Expire it (TTL). Accept that data may be up to N seconds old. Simple and safe; pick N by asking “how stale can this be before anyone minds?” Product lists: minutes. Stock levels on the checkout page: don’t cache.

  • Delete it when the data changes. When a product is updated, delete the products key, and the next request reloads it:

    JavaScript
    async function updateProduct(id, changes) {
      await db.updateProduct(id, changes);
      cache.delete("products");
    }

    Delete rather than update the cached copy: it’s simpler and avoids two requests writing different versions.

Most real systems use both: delete on change for freshness, plus a TTL as a safety net in case a delete is missed.

Common mistakes#

  • Caching personal data in a shared cache. If the cache key doesn’t include the user, one person can see another’s data. Put the user ID in the key, or don’t cache it.
  • Caching errors. If the database is briefly down and you cache the empty result for an hour, the outage lasts an hour. Only cache successful results.
  • The thundering herd. When a popular key expires, hundreds of requests miss at once and all hit the database together. Fixes: let one request reload while the others wait for it, or serve the old value while refreshing in the background (stale-while-revalidate).
  • Caching before measuring. If a query is slow because it’s missing an index, fix that first. A cache over a slow query hides the problem until the cache is cold.

What to cache first#

Look for things that are read often, change rarely, and are slow to produce:

  • Static files: images, CSS, JavaScript, fonts (browser and CDN, long TTL).
  • Public pages and API responses that are the same for everyone (CDN or app cache, short TTL).
  • Results from slow or rate-limited external APIs, such as exchange rates or a shipping price list (app cache).
  • Expensive calculations, such as reports and dashboards (app cache, refreshed on a schedule).

Caching is one of the main tools in how to scale a web app. For Wix sites, much of the browser and CDN caching is handled for you, and why your Wix site is slow covers what you can still control. If you need a slow API or integration made fast, that’s part of my custom API integration work.

Comments