AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 08 — Caching — Trading Freshness for Speed, Deliberately

08 — Caching — Trading Freshness for Speed, Deliberately

August 13, 20267 min read
Download as Markdown

"Just add Redis and it'll be fast" was my caching philosophy, and it produced either no speedup or stale data. The fix was seeing the trade underneath: caching is a deliberate trade of freshness for speed, and every cache decision is really two answers — what layer holds the copy, and how do we know when it's stale. [1] Get either wrong and you either gain nothing or serve wrong data.

The frame that helped is that a cache is a second copy of data kept closer to where it's needed, so the slow original doesn't have to be touched. Every layer of the backend stack can hold a cache — the browser, a CDN, a reverse proxy, an in-memory store, even the database's own buffer pool. The question is never "should I cache" (you almost always should, somewhere); it's "where, and how do I invalidate it."

request Client-side cache — browser / HTTP cache HTTP headers (Cache-Control, ETag), service workers HIT? miss Server-side cache — Redis / Memcached in-memory key-value, sub-millisecond reads HIT? miss Origin — the database (slow, authoritative) the source of truth trade: freshness ▸ speed

The layers, top to bottom

Client-side caching is the outermost layer — the browser itself holds copies of responses based on HTTP caching headers [4][5]. Cache-Control: max-age=3600 tells the browser the response is fresh for an hour; ETag lets it check whether its cached copy is still valid with a lightweight conditional request. This layer is the cheapest cache (the data is already where it's needed; no network at all on a hit), and it's controlled entirely by response headers the server sets.

Server-side in-memory caching is the layer I'd add when client-side caching isn't enough — when many clients need the same expensive computation, or when the data changes more often than the browser can tolerate but less often than every request. This is where Redis and Memcached live [2][3].

  • Redis is an in-memory data structure store — strings, lists, sets, hashes, sorted sets — with optional persistence, replication, and clustering. It's not just a cache; it can be a session store, a message broker, a rate limiter. Sub-millisecond reads. Redis is the modern default for server-side caching because of its rich data types and durability options.
  • Memcached is the simpler predecessor — a distributed in-memory hash table, intentionally limited to key-value strings, with LRU eviction. It does one thing (cache) very well and is lighter than Redis. For pure key-value caching with no need for data structures or persistence, Memcached is still a fine choice.

The database's own caches are the innermost layer — Postgres/MySQL buffer pools, query caches — which I tune rarely and almost never override at the application level.

The harder question: invalidation

The hard part of caching isn't storing the copy; it's knowing when it's wrong. There's a famous line that "there are only two hard problems in computer science: cache invalidation and naming things." The roadmap's caching material gestures at the strategies, and they cluster into a few patterns:

  • TTL (time-to-live). Each cached entry expires after a fixed duration. Simple, robust, and stale-by-design — the data may be up to TTL seconds old. Best for data that changes slowly and tolerates some lag (a blog post body, a config value).
  • Write-through / explicit invalidation. Whenever the underlying data changes, the code updates or deletes the cache entry immediately. Fresh, but requires every write path to remember to invalidate — miss one and you serve stale data indefinitely.
  • Tag-based invalidation. Cache entries are tagged with categories; when a category changes, all entries with that tag are purged at once. This is what I use in the portfolio (revalidateTag('prismic:blog') purges all blog data) because it groups related entries under one invalidation call.

The choice is a freshness/complexity trade-off. TTL is simplest and stale-tolerant; explicit invalidation is freshest and bug-prone; tag-based sits between. The worst cache bug is silent staleness — the user sees old data and nobody realizes it's wrong. That's why I prefer TTL with a short ceiling even when I also do explicit invalidation: a stale entry can't outlive its TTL.

Client-side caching specifics: HTTP headers

Because client-side caching is free and header-driven, it's worth knowing the two headers that matter most:

  • Cache-Control — the modern directive. max-age=N sets freshness in seconds; public vs private controls whether shared caches (CDNs) can store it; no-cache means "revalidate every time"; no-store means "don't cache at all."
  • ETag / If-None-Match — a content hash. The server sends an ETag with the response; the browser sends it back as If-None-Match on the next request. If the content hasn't changed, the server replies 304 Not Modified with no body — saving bandwidth but still costing a round trip.

The portfolio uses Next.js's ISR (Incremental Static Regeneration), which is essentially server-side caching with TTL plus tag-based invalidation — pages are statically generated, served from cache, and revalidated either on a timer or when a tag is purged. It's the same patterns, packaged.

How I use this

Three rules capture the practical takeaway:

  • Cache at the outermost layer that can serve the request. Browser cache before CDN cache before server cache before DB. The earlier the hit, the cheaper.
  • Default to TTL with a short ceiling, layer explicit invalidation on top. TTL guarantees eventual freshness even when an invalidation path is missed; explicit invalidation keeps the typical case fresh.
  • Measure before adding Redis. A slow endpoint is often a missing index or an N+1, not a missing cache. Caching a broken query hides the problem and adds a second source of truth to manage. Fix the query first; cache the result once it's actually expensive.

The framing — freshness for speed, deliberately — is what keeps caching disciplined. A cache is a contract with the user that the data they see may be slightly old, in exchange for it being fast. Making that trade deliberately, at the right layer, with a real invalidation story, is the whole skill.

References

[1] Cloudflare, "What is caching?," 2024. [Online]. Available: https://www.cloudflare.com/en-gb/learning/cdn/what-is-caching/

[2] Redis Ltd., "Redis Documentation." [Online]. Available: https://redis.io/docs/latest/

[3] "memcached/memcached," GitHub. [Online]. Available: https://github.com/memcached/memcached#readme

[4] Redis Ltd., "Client Side Caching." [Online]. Available: https://redis.io/docs/latest/develop/use/client-side-caching/

[5] "Top Caching Strategies Explained," ByteByteGo. [Online]. Available: https://blog.bytebytego.com/p/top-caching-strategies

Knowledge check · Question 1 of 5

Every cache decision is really two answers. What are they?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!