Live · Tue, Nov 3, 2026 · 01:02 UTC Block 843,917 Fees 14 sat/vB Fear & Greed 72 · Greed
Newsletter Pro Terminal Sign in
ITop Field News.
Subscribe →
Live · 01:02 UTC Block 843,917 F&G 72
Software development Software development desk

Caching strategies in web APIs: what most dev teams get wrong

API caching is one of the highest-impact performance controls available to dev teams, but most organisations bolt it on after the problems appear. Here's what actually goes wrong and how to fix it.

Contemporary computer on support between telecommunication racks and cabinets in modern data center

Photo by Brett Sayles on Pexels

Caching in web APIs sounds deceptively simple: store a response, return it again, skip the expensive work. In practice, most development teams in Australia implement caching reactively, after latency complaints or runaway cloud bills force the issue. By then, the cache is bolted onto an architecture that wasn't designed for it, and the resulting mess creates bugs that are much harder to diagnose than the original performance problem.

There are four broad caching strategies for web APIs, and picking the wrong one for your access pattern is more damaging than having no cache at all.

Cache-aside vs read-through: the distinction most teams ignore

Cache-aside (sometimes called lazy loading) is the most common pattern. The application checks the cache first, fetches from the data store on a miss, then writes the result back to the cache. It's explicit and easy to reason about. The downside is that every cache miss results in a read-plus-write operation, and during cold starts or cache flushes your database takes the full load.

Read-through caching delegates the fill logic to the cache layer itself. On a miss, the cache fetches the data from the backing store without the application having to manage that logic. This simplifies application code but introduces tighter coupling between your caching infrastructure and your data store. Teams running complex API designs with retry semantics often underestimate this coupling and end up with duplicate fetch operations during transient failures.

Write-through caching updates the cache whenever the underlying data is written. This keeps cached data fresher but adds write latency. Write-behind (or write-back) caching accepts the write into the cache first and persists to the store asynchronously. That cuts write latency dramatically but risks data loss if the cache node fails before the write is flushed. For APIs handling financial or health data, write-behind is almost never appropriate without a durable write-ahead log sitting in front of it.

TTL is not a substitute for invalidation logic

Time-to-live is the most overused and most misapplied caching primitive. A TTL tells the cache how long to keep something. It does not tell the cache when that thing is no longer correct. Teams that rely purely on TTL are accepting a window of stale data equal to the TTL value. For most read-heavy API responses this is fine. For anything involving user-specific state, permissions, or prices, it is not.

Event-driven invalidation is the more precise alternative. When a record changes in your system of record, a message hits a queue, and the cache entry for that record is explicitly deleted or updated. This approach requires more infrastructure: a message broker, consumers that understand your cache key schema, and coordination between the service writing the data and the service owning the cache. It's worth the investment when stale data has business consequences. It's overkill for a public product catalogue that refreshes nightly.

Cache stampede is the failure mode that bites teams who rely only on TTL with no lock-ahead or probabilistic early expiry. When a popular cache key expires and hundreds of requests hit the backing store simultaneously, the database can buckle under the sudden load spike. The standard remedies are mutex locking on cache miss (one request fetches, others wait), background refresh before expiry, or the XFetch algorithm that probabilistically refreshes entries before their TTL ends. None of these require framework changes. They require forethought.

Cache key design is where subtle bugs live

A cache key that doesn't capture all the dimensions of a request will return the wrong data to some users. This is the most pernicious class of caching bug because it's often invisible in testing and surfaces only in production when two users with different attributes hit the same key.

Common offenders include:

  • Forgetting to include the Accept-Language header in the key for APIs serving localised content
  • Omitting tenant ID in multi-tenant SaaS APIs, causing cross-tenant data leakage
  • Including volatile parameters (timestamps, request IDs) in the key, effectively making the cache useless
  • Not normalising query parameter order, so ?sort=asc&filter=active and ?filter=active&sort=asc generate separate cache entries for identical responses

The fix for most of these is a canonical key builder: a function that takes all the semantically meaningful dimensions of a request and produces a deterministic, minimal key. Review every field that goes into the key against two questions. Does this field change the response? Does this field need to be represented differently than its raw form? Anything that fails the first question should be removed; anything that fails the second should be normalised before keying.

Where HTTP cache headers actually matter

For public or shared API responses, HTTP cache semantics (Cache-Control, ETag, Vary) let CDNs and proxies participate in caching without any application-side cache store. This is dramatically cheaper than hitting your application servers for every read, and it's the mechanism most teams overlook when they jump straight to Redis.

RFC 9111 defines HTTP caching semantics, and the Vary header is where most implementations go wrong. Vary tells caches which request headers differentiate responses. If your API serves different content based on the Authorization header, you must include Vary: Authorization or shared caches will return one user's data to another. If you include too many headers in Vary, caches can't effectively share entries and your hit rate collapses.

Conditional requests (If-None-Match with ETags, If-Modified-Since with Last-Modified) let clients validate whether their cached copy is still current without re-downloading the full response. This halves bandwidth for clients that cache well. Few internal API consumers bother implementing conditional requests because the latency savings feel marginal on a LAN. At the edge, or for mobile clients on Australian 5G networks with variable signal, they matter more than teams assume.

Distributed cache consistency across services

Single-service caching is tractable. Distributed caching across a microservices boundary is where consistency guarantees get complicated. If Service A owns a cache of user records and Service B writes user updates, you have a cross-service invalidation problem. The write happens in B's domain; the stale entry sits in A's cache. Without explicit coordination, A serves stale data until its TTL expires.

Teams building event-driven architectures often solve this with a shared invalidation topic: any service that mutates a cacheable entity publishes an invalidation event, and any service that caches that entity type subscribes. It's a clean pattern but it means your cache invalidation logic is now part of your event schema, and schema changes need to be coordinated. This is a specific case of the broader contract problem that contract testing addresses between services.

Another approach is to make cached data immutable by versioning it. Instead of invalidating a cache entry for user:42, you write new data to user:42:v2 and update a pointer. Reads always follow the pointer; old versions expire naturally. This sidesteps invalidation races but increases storage and requires pointer management logic. It's most useful when cache entries are large and writes are infrequent.

Observability for your cache layer

A cache without observability is a liability. You can't tune what you can't measure. At a minimum, teams should be tracking hit rate by endpoint, cache miss latency against backing-store latency, eviction counts (which indicate memory pressure), and key distribution (to spot hot keys that a single cache node is absorbing).

Hit rate below 80% for a steady-state workload usually signals a key design problem, a TTL that's too short, or a cache that's being evicted under memory pressure. The first thing to check is always evictions per second against your configured maxmemory policy. If evictions are high, your cache is undersized for the working set, and adding more memory is cheaper than debugging key design.

Structured logging that ties cache hits and misses to trace IDs makes it possible to correlate cache behaviour with overall request latency. This connects directly to the kind of distributed tracing discipline covered in logging best practices for distributed systems, where correlation IDs are the thread that ties a request across services.

What to actually do first

If your team is starting from scratch, don't reach for Redis before you've confirmed that HTTP cache headers won't solve the problem. A CDN in front of your API with correct Cache-Control headers can eliminate the majority of read load on public endpoints with near-zero code change.

If you're already running an application-side cache and hitting correctness bugs, audit your cache key builder first. Print out the keys being generated in a staging environment with diverse request inputs and check them manually. Most key bugs are obvious once you look.

If you're hitting stampede-style load spikes, add mutex locking on cache miss before reaching for probabilistic refresh. Mutex locking is easier to implement correctly and easier to reason about under pressure. Probabilistic refresh algorithms require tuning the beta parameter against your actual TTL distribution, which is not a task for an incident.

Caching is not a performance shortcut. It's a consistency trade-off. Every cache entry is a promise that the stored data is close enough to current for the use case it serves. Get the boundaries of that promise wrong, and the cache becomes the bug.

→ The Confirmations · Daily newsletter

One email at 06:00 UTC. Six minutes. The only digest written for desks, not for retail.