Stockio

ETag Caching for APIs: Cache-Control and Redis

Build a safe API caching policy by separating HTTP freshness, conditional ETag validation, and Redis data reuse, with explicit rules for privacy and invalidation.

By Chidi Nwosu

Backend Engineer ·

On this page

API caching has three separate jobs: avoiding requests while a representation is fresh, avoiding body transfers when a representation is unchanged, and avoiding expensive backend work. Cache-Control, ETags, and Redis address different parts of that problem. Combining them safely requires clear ownership of freshness and user-specific data.

Start with a response classification

Use the API latency tester to record a baseline for a non-sensitive read endpoint before adding caching. Compare status and timing across repeated requests, but inspect response headers separately in your HTTP client. Faster responses alone do not establish whether a browser cache, intermediary, or Redis served the result.

Classify each route as public, user-specific, or too sensitive to retain. A public product catalog might tolerate a short freshness window. An account balance needs an explicit consistency decision. A token exchange should generally avoid storage by HTTP caches. Write the policy alongside the endpoint contract.

Define a staleness budget in business terms. Ask how long a removed item may remain visible, whether a newly saved setting must appear immediately, and which mutations affect each representation. Do not choose a five-minute lifetime merely because another service uses one.

Also define the source of truth. Redis should not quietly become the authoritative store for data that originally belonged in a transactional database. Cache loss must be recoverable, and eviction must not erase business records.

Express freshness and privacy with Cache-Control

Cache-Control tells HTTP caches how a response may be stored and reused. A public catalog response might use public, max-age=30, s-maxage=120: a browser can reuse it for thirty seconds, while a shared cache receives a separate two-minute freshness lifetime. Apply this only when every requester may receive the same representation.

For user-specific data, private, no-cache permits a private cache to store the response but requires validation before reuse. No-cache is not no-store. Use no-store when the policy is that caches must not retain the response, particularly for sensitive authentication material. Review the full semantics in the MDN Cache-Control reference.

Include representation dimensions in the cache policy. If language selection depends on Accept-Language, a cache must distinguish those variants, often with Vary. Avoid adding every request header indiscriminately: high-cardinality keys can destroy reuse. Never treat Vary: Cookie as a substitute for deciding whether shared storage is safe.

Authenticated requests require particular care because shared-cache rules interact with authorization and response directives. Prefer an explicit private policy unless you have designed and tested a public representation. A single cross-user cache leak can outweigh every performance improvement.

Use ETags for conditional validation

An ETag identifies a selected representation. When the client later sends If-None-Match, the server can return a bodyless 304 Not Modified if the current representation matches. This saves transfer, but it saves database work only if the validator can be checked cheaply.

A framework-neutral Node-style example can validate serialized public data. The handler assumes a public catalog and a complete representation in memory:

import { createHash } from "node:crypto";

function sendCatalog(req, res, catalog) {
const body = JSON.stringify(catalog);
const digest = createHash("sha256").update(body).digest("hex");
const etag = 'W/"' + digest + '"';
res.setHeader("ETag", etag);
res.setHeader("Cache-Control", "public, max-age=30");

// Deliberately handles only a single exact validator.
if (req.headers["if-none-match"] === etag) {
  res.statusCode = 304;
  return res.end();
}
res.setHeader("Content-Type", "application/json");
res.end(body);
}

Production conditional-request handling should use a tested parser or framework support: If-None-Match can contain multiple validators or a wildcard, and its comparison rules are not simple string equality. Preserve relevant cache metadata on the 304 response. See the MDN If-None-Match reference for those details.

The example uses a weak validator, suitable for cache validation rather than byte-level range guarantees. If using a database revision instead, ensure it changes whenever any field in the response changes. ETags are validators, not secret tokens or authorization controls.

Add Redis for expensive application work

Cache-aside means the application checks Redis first, queries its source on a miss, then stores the result with a bounded lifetime. This can reduce database work even when the HTTP response itself is private or requires revalidation. Consult the Redis caching documentation for the broader pattern.

Build keys from every input that affects the cached value: tenant, resource identifier, locale, filters, and representation schema. Cache public data separately from user-specific projections. Authenticate and authorize before returning a cached object; a cache hit must never bypass access checks.

Use bounded TTLs and small randomized expiry offsets to reduce synchronized misses. For popular expensive keys, use a short-lived coordination mechanism or request coalescing so that one miss does not trigger hundreds of database queries. If Redis is unavailable, decide whether the endpoint can fall back safely without overwhelming the database.

Cache empty results briefly only when the semantics are safe. An absent product may soon be created, and a permission-denied response must not become a shared negative entry. Monitor memory, eviction, serialization failures, and hit rates alongside latency. A high hit ratio is not success if cached values are wrong.

Design invalidation and consistency explicitly

After a successful mutation, invalidate affected keys or publish new versioned keys. Update the authoritative database before announcing a new cached representation. Recognize the race: an older in-flight read can repopulate stale data after deletion. Strict requirements may need revision checks, coordinated writes, or another consistency mechanism rather than simple delete-on-write.

Redis expiry and HTTP freshness operate independently. Deleting a Redis key does not recall a response already considered fresh by a browser. Set HTTP lifetimes to match the real business budget and provide a deliberate read-after-write strategy, such as returning the updated representation directly from the mutation.

Test cache isolation with two users and two tenants, then test expiry, failed invalidation, and Redis downtime. Use a JSON formatter to compare expected representations during debugging. Record the complete policy in your backend API guides, including which layer owns each lifetime and how operators disable caching during an incident.

Key takeaways

  • Separate HTTP freshness, conditional validation, and application data caching.
  • Treat privacy classification as a prerequisite for cache reuse.
  • Make validators cover the entire selected representation.
  • Include tenant and representation dimensions in Redis keys.
  • Test stale-read races and remember that Redis invalidation cannot purge browser freshness.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in Backend & APIs
Backend & APIs

12 Best Free Public APIs for Side Projects

Find approachable public APIs for weather, books, countries, and sample data, plus practical notes on access, limits, and choosing an endpoint for a side project.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

10 Best Backend-as-a-Service (BaaS) Platforms

Compare ten backend services for app data, sign-in, and server functions, with plain-language strengths, trade-offs, and advice for choosing a platform that fits your project.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

CSRF Protection With SameSite Cookies, Tokens and Origins

Protect cookie-authenticated API mutations with CSRF tokens, origin validation, SameSite cookies, and clear middleware rules that survive real browser login flows.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

How to Fix CORS Preflight Errors in Your API

Diagnose CORS preflight failures with explicit origins, credential rules, OPTIONS responses, and cache behavior while keeping authentication and authorization separate.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

API Rate Limiting With Token Buckets and Redis

Design API rate limits with token buckets, atomic Redis operations, fair identities, and Retry-After responses without confusing quotas with concurrency controls.

Includes: API latencyChidi NwosuChidi Nwosu
Backend & APIs

REST vs GraphQL vs tRPC: Choosing an API Style

Compare REST, GraphQL, and tRPC through client needs, caching, security, and deployment constraints, then validate your choice with a production-shaped prototype.

Includes: API latencyChidi NwosuChidi Nwosu