Call
Redis

API Caching in Production: Cache-Control, ETags, CDNs, and Redis

Design API caching across browsers, CDNs, gateways, and Redis with correct Cache-Control directives, ETags, cache keys, invalidation, privacy boundaries, and stampede protection.

Published: January 15, 2026
Updated: September 12, 2026
11 min read
API Caching in Production: Cache-Control, ETags, CDNs, and Redis

API Caching in Production: Cache-Control, ETags, CDNs, and Redis

API caching can happen at several layers: inside the browser, at a CDN or reverse proxy, in an API gateway, or inside the application with Redis. Those layers solve different problems, and treating them as one generic "cache" is how stale data, privacy leaks, and confusing invalidation bugs appear.

The first design question is therefore not "which Redis command should I use?" It is where should this response be cached, who is allowed to reuse it, and how fresh must it be?

Four common API cache layers

LayerBest forMain risk
Browser/private HTTP cacheresponses reusable by one clientstale client state
CDN/shared proxypublic responses reusable across many clientsleaking personalized data
API gatewaycentralized route-level response cachingoverly broad cache keys
Redis/application cacheexpensive upstream or computed responsesapplication-managed invalidation

Many production systems use more than one layer. A public catalog endpoint might be cached at the CDN and also use Redis behind the origin. A personalized account endpoint might use only a private browser cache or no HTTP caching at all.

Start with HTTP semantics before adding Redis

HTTP already has a standardized caching model defined by RFC 9111. For cacheable API responses, the most important control is usually the Cache-Control response header.

A public response that may be reused for one minute can look like:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60

{"id":42,"name":"Keyboard"}

A response containing user-specific data should normally not be reusable by a shared CDN cache:

Cache-Control: private, max-age=60

And when a response may be stored but must be revalidated before reuse:

Cache-Control: no-cache

no-cache does not mean "do not store." It means the stored response must be validated before it can be reused. If storage itself must be prohibited, no-store is the relevant directive.

That distinction matters for authenticated APIs, debugging, and compliance-sensitive data.

Public and private are security decisions

A shared cache can serve one stored response to many users. That is excellent for public data and dangerous for personalized data.

Before marking an API response public, ask whether the body varies by:

  • authenticated user
  • tenant or organization
  • subscription plan
  • authorization scope
  • locale
  • currency
  • feature flags
  • request headers

If the response differs by security context, either keep it out of shared caches or make the cache key correctly include the relevant variation. A fast response delivered to the wrong user is a data leak, not a performance optimization.

Use Vary when request headers change the representation

HTTP's Vary response header tells shared caches which request headers participate in choosing the correct stored response.

For an API that returns different content encodings:

Vary: Accept-Encoding

If an API intentionally returns different representations based on another request header, that header may also need to appear in Vary.

Do not casually use Vary: * or vary on many high-cardinality headers. Every added dimension reduces cache reuse and increases the number of stored variants.

Authorization deserves special care. Do not assume adding an authorization header to a key automatically makes a shared-cache design safe. Personalized authenticated responses are commonly better kept private unless the caching architecture was explicitly designed for them.

ETags reduce payload transfer without serving stale data blindly

Some API responses change infrequently but still need freshness checks. HTTP validators solve this without forcing the full body to be transferred every time.

The server returns an ETag:

HTTP/1.1 200 OK
ETag: "product-42-v17"
Cache-Control: no-cache
Content-Type: application/json

Later, the client or intermediary can ask whether that representation is still current:

If-None-Match: "product-42-v17"

If nothing changed, the server can return:

HTTP/1.1 304 Not Modified

The cached body is then reused without downloading the representation again.

RFC 9111 defines this validation flow, and current MDN guidance explains ETag / If-None-Match as the preferred validator mechanism when an entity tag is available.

Last-Modified is useful but less precise

A server can also send:

Last-Modified: Sat, 12 Sep 2026 05:30:00 GMT

A client may later revalidate with:

If-Modified-Since: Sat, 12 Sep 2026 05:30:00 GMT

This can be useful when the underlying resource already has a trustworthy modification timestamp. ETags are often easier to make representation-specific, especially when API output combines several underlying records.

If both validators are present during normal cache validation, If-None-Match takes precedence over If-Modified-Since according to HTTP semantics.

CDN caching works best for broadly reusable responses

A CDN can remove enormous amounts of traffic from the origin when many users request the same public representation.

Good candidates include:

  • public product/category data
  • documentation APIs
  • public configuration that changes infrequently
  • geographic/reference datasets
  • anonymous search results with bounded variation

Poor candidates include:

  • account pages
  • private invoices
  • per-user permissions
  • mutable checkout state
  • responses whose output is difficult to key safely

When supported by your CDN, shared-cache freshness can be controlled separately from browser freshness. The exact directives and behavior vary by provider, so verify the CDN's documentation instead of assuming every intermediary interprets provider-specific extensions identically.

Redis solves a different caching problem

HTTP caches reuse HTTP responses according to protocol semantics. Redis usually caches application data or serialized responses inside your backend.

Redis is especially useful when:

  • an upstream API is slow or rate-limited
  • generating the response requires expensive database or service calls
  • several application instances should share one cached result
  • the response cannot be effectively cached by clients/CDNs
  • the backend needs explicit invalidation control

A simple application response cache can look like:

key = api-cache:weather:agra
cached = Redis GET key

if cached exists:
    return cached

response = call upstream API
Redis SET key response EX 120
return response

Redis' current official API-caching and cache-aside material uses the same core model: read Redis first, call the slower origin on a miss, and store the result with a TTL.

Build Redis API keys from response semantics

Do not key a Redis response cache only by route name if query parameters or user context change the result.

For example:

api-cache:products:category:keyboards:page:2:currency:INR

A stable cache key may need normalized versions of:

  • route/path
  • relevant query parameters
  • tenant ID
  • locale
  • currency
  • API version
  • authorization scope if caching private data deliberately

Normalize parameters so logically identical requests do not create separate entries merely because query-string ordering differs.

Never cache unsafe responses accidentally

Before caching a response, define which methods and status codes are eligible.

A conservative application cache often begins with successful GET responses only.

Be careful with:

  • POST, PATCH, PUT, and DELETE
  • authentication endpoints
  • one-time tokens
  • signed URLs
  • password-reset flows
  • rapidly changing authorization results
  • transient upstream errors

RFC 9111 requires unsafe requests to reach the origin rather than being answered directly from a normal HTTP cache. Application-level Redis caches should follow similarly deliberate semantics instead of caching arbitrary handlers.

Decide whether errors are cacheable

Caching a successful expensive response for two minutes may be useful. Caching a temporary upstream 500 for two minutes may turn a brief outage into an artificially prolonged outage.

Possible policy:

  • cache stable 200 responses normally
  • optionally cache genuine 404 responses for a short period when repeated misses are expensive
  • avoid caching transient 5xx failures unless you intentionally implement stale-on-error behavior
  • use shorter TTLs for uncertain or rapidly changing results

The important point is to distinguish a business result from a dependency failure.

Invalidation should follow the owning source of truth

If a PUT /products/42 changes data that also backs GET /products/42, the write path should invalidate the relevant application cache after the durable write succeeds.

For Redis:

update primary database
DEL api-cache:product:42

If a CDN also caches that response, you need either:

  • a sufficiently short shared TTL
  • a provider-supported purge/invalidation mechanism
  • versioned URLs or representations where appropriate

Do not assume deleting Redis automatically clears browser and CDN copies. Cache layers are independent unless you explicitly coordinate them.

Avoid synchronized expiration and cache stampedes

When a popular API cache entry expires, hundreds of requests can miss simultaneously and all call the same database or upstream service.

Mitigations include:

  • request coalescing / single-flight rebuilding
  • a short distributed lock for hot entries
  • TTL jitter so many keys do not expire at once
  • proactive refresh for a small number of extremely hot keys
  • stale-while-revalidate behavior where the chosen cache layer supports it and stale data is acceptable

The mechanism matters less than defining the failure mode before peak traffic finds it for you.

stale-while-revalidate can hide refresh latency

RFC 5861 defines the stale-while-revalidate extension used by many HTTP caching systems. It permits a cache, within the declared window, to serve a stale representation while revalidating it asynchronously.

A representative header can look like:

Cache-Control: public, max-age=60, stale-while-revalidate=30

For appropriate public endpoints, this can keep latency stable while the cache refreshes.

Do not use stale serving for data whose freshness is security-critical, transactional, or legally significant merely to improve response time.

Authenticated APIs need a stricter default

For endpoints returning private user data, a safer starting point is often:

Cache-Control: private, no-cache

or, when storage must be prohibited:

Cache-Control: no-store

Then decide separately whether an internal Redis cache can safely cache some lower-level shared data used to construct the personalized response.

For example, a user's order page may not belong in a shared CDN cache, while the public product metadata referenced by those orders can still be cached aggressively.

This layered design usually provides better reuse than caching complete personalized responses.

Do not confuse HTTP Age with your Redis TTL

Shared HTTP caches can send an Age header indicating how long the stored response has resided in a cache according to HTTP age calculations.

Redis TTL tells your application how long a Redis key has before expiry.

They are independent clocks in different cache layers. Debugging becomes much easier when observability identifies which layer produced the hit.

Useful response diagnostics in controlled environments include headers such as:

X-Cache: HIT
X-Cache-Layer: edge

Use provider-native cache-status headers where available rather than inventing a permanent public header contract unless you actually need one.

Measure each cache layer separately

Track at least:

  • browser/CDN/gateway hit rate where visible
  • Redis hit and miss rate
  • origin request volume
  • upstream API request volume
  • response freshness / age
  • cache fill latency
  • purge/invalidation failures
  • Redis errors and fallbacks
  • stampede coordination waits
  • bytes transferred for revalidated versus full responses

A high Redis hit rate is not very meaningful if the CDN could have served the request without reaching your application at all.

Likewise, a high CDN hit rate does not prove personalized endpoints are keyed safely.

A practical decision framework

Use browser or private HTTP caching when

  • one client can safely reuse its own response
  • protocol-native validation with ETags is valuable
  • the response is personalized but safe to keep in a private cache

Use a CDN/shared HTTP cache when

  • the response is public or deliberately shareable
  • many clients request the same representation
  • origin offload matters

Use Redis/application response caching when

  • producing the response is expensive inside the backend
  • multiple app instances need a shared cached result
  • the upstream is slow, costly, or rate-limited
  • application-controlled invalidation is required

Use no cache when

  • correctness requires the latest committed state
  • the response is security-sensitive and storage adds unacceptable risk
  • request variation makes safe keys impractical
  • cache invalidation is harder than simply serving the origin query

Production checklist

Before enabling caching on an API route, answer:

  • Is the response public, private, or non-cacheable?
  • Which cache layers are allowed to store it?
  • What request fields change the representation?
  • What is the acceptable freshness window?
  • Can ETag or Last-Modified revalidation reduce transfers?
  • If Redis is used, what is the deterministic key?
  • What invalidates the entry after a write?
  • What happens when Redis, the CDN, or the upstream is unavailable?
  • Can stale data be served, and for how long?
  • How are stampedes controlled?
  • How will we prove that one user's response can never be served to another user?

Good API caching is not "cache everything." It is a freshness and reuse contract shared by the client, intermediaries, application, and source of truth.

References

Tags:API CachingHTTPRedisCDNCache-ControlETagBackend Architecture
ENTERPRISE SAAS ENGINEERING

BUILDING A SAAS PRODUCT?

Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.

EXPLORE SAAS SERVICES →