Representative interview topic

Backend interview: How would you design a safe stale-while-revalidate cache contract?

BackendHard
Offer.cc Editorial TeamPublished Updated

Question

A read-heavy API needs low latency during origin slowness. How would you use stale-while-revalidate and stale-if-error, and where would you refuse to serve stale data?

Prompt and setting

You operate GET /catalog behind a CDN and an application cache. The origin can be slow during deploys, but customers prefer a slightly old catalog to a blank page. Design cache headers and the revalidation path while keeping tenant data isolated and freshness measurable.

Assume catalog responses are public per tenant, writes go through the origin, and an emergency price change must become visible quickly. The answer must distinguish an availability fallback from permission to serve sensitive state.

What the interviewer tests

  • Whether you understand freshness, staleness, revalidation, and the separate stale-if-error policy.
  • Whether cache keys vary on tenant, authorization, locale, and content negotiation.
  • Whether concurrent misses trigger one origin request or a thundering herd.
  • Whether an operator can revoke stale serving and prove freshness with metrics.

Clarifying questions before answering

  1. Is the response public, tenant-scoped, or user-specific? Private data may not be shared by a CDN at all.
  2. What is the maximum tolerated age for normal reads and for an origin outage? These become separate freshness and stale windows.
  3. Can a price or permission change invalidate the object immediately? If yes, add purge or versioned keys rather than trusting TTL alone.
  4. Are validators available? ETag or Last-Modified changes a full refetch into a conditional revalidation.

30-second answer framework

“I define a fresh window, a bounded stale-while-revalidate window, and a separate stale-if-error window. The cache key includes every representation and authorization boundary; user-specific data is private. A stale hit returns quickly and triggers one background conditional request, while concurrent misses are coalesced. Emergency changes purge or version the key. Metrics expose age, revalidation outcomes, stale-if-error use, and tenant leakage tests, and an operator can disable stale serving.”

Step-by-step deep dive

1. Separate freshness from availability

max-age defines how long a stored response is fresh. stale-while-revalidate permits a cache to serve a stale response for a bounded interval while it revalidates in the background. stale-if-error is a separate availability allowance for an origin error. Neither directive makes stale data correct, and a response with must-revalidate or an applicable no-cache rule cannot be reused casually.

A public catalog might use a short fresh window and a longer bounded stale window. A permissions endpoint, account balance, or emergency price should use private, no-store, a purge path, or a much stricter policy. The business risk decides the window, not a cache default.

2. Build a safe cache key and response contract

The key must include tenant, locale, encoding, and any request header named by Vary. Never let an authenticated response fall into a shared cache unless the representation is explicitly public and authorization-independent. A response can state its policy clearly:

http
Cache-Control: public, max-age=30, stale-while-revalidate=120, stale-if-error=600
Vary: Accept-Encoding, Accept-Language, X-Tenant-ID
ETag: "catalog-tenant-7-v42"

The server must validate that X-Tenant-ID is derived from the authenticated route or host, not an arbitrary client value. If a tenant boundary cannot be represented safely in the key, disable shared caching.

3. Revalidate without a herd

On a stale hit, return the stored body and enqueue one revalidation per cache key. Use a short lock or single-flight map so ten thousand readers do not create ten thousand origin calls. The revalidator sends If-None-Match; a 304 Not Modified refreshes freshness without replacing the body, while a new 200 replaces the object and validator.

If revalidation fails with a transient error, keep the old object only within the stale-if-error bound. Record the failure and age. Do not extend the stale window indefinitely by repeatedly resetting its timer.

4. Make invalidation explicit

TTL is a safety net, not an emergency control. A price or permission change should publish a versioned invalidation event or purge affected keys. The write path can commit the new version before publishing the event; consumers should be idempotent and replayable. If the purge cannot be confirmed, the API can attach a short must-revalidate period or bypass cache for the affected tenant.

5. Measure and operate the policy

Track response age, fresh-hit ratio, stale-while-revalidate hit ratio, stale-if-error count, revalidation latency, 304 rate, origin error rate, lock contention, and cache-key cardinality. Alert on stale age approaching the maximum, unexpected stale-if-error spikes, and cross-tenant test failures.

Provide a feature flag or route-level kill switch to stop serving stale responses. Test cold misses, concurrent stale hits, origin timeouts, validator changes, purge races, tenant headers, locale variants, and an emergency price update. The test oracle is the freshness and isolation contract, not only a lower latency number.

High-quality sample answer

I would start by classifying the data. A public, tenant-scoped catalog can have max-age=30, stale-while-revalidate=120, and a separately justified stale-if-error=600; user-specific or permission data should be private or uncached. The key includes tenant and representation dimensions, and the response carries a validator.

On a stale hit, I serve the body and run one conditional revalidation per key. 304 refreshes freshness, and 200 replaces the object. A transient origin failure may use the bounded stale-if-error window, never an endlessly reset timer. Writes publish idempotent purge or version events for urgent changes. I monitor age, stale usage, revalidation outcomes, and tenant isolation, with a kill switch for stale serving.

Common mistakes

  • Error: Applying stale-while-revalidate to account or permission data → Why it fails: a fast stale response can expose an invalid authorization decision → Fix: keep sensitive data private or uncached.
  • Error: Omitting tenant or locale from the key → Why it fails: one representation can be served to another boundary → Fix: derive and test every key dimension.
  • Error: Refreshing the stale timer after every failed revalidation → Why it fails: outage data can persist forever → Fix: enforce an absolute stale deadline.
  • Error: Sending one origin request per stale reader → Why it fails: a stale burst becomes a thundering herd → Fix: use single-flight revalidation per key.
  • Error: Treating TTL as emergency invalidation → Why it fails: urgent changes wait for expiry → Fix: publish purge or version events and verify completion.

Follow-up questions and responses

Why not use only stale-if-error?

It helps only when the origin request encounters an error. stale-while-revalidate improves normal latency by serving an old response while a successful refresh runs. They solve different conditions and need separate bounds and metrics.

What happens when the validator changes during a purge?

Version the object and make the purge event idempotent. A revalidation that sees the old validator must not overwrite a newer version; compare object versions or commit timestamps before replacing the cache entry. If ordering is uncertain, bypass the cache briefly for that key.

Can a CDN cache an authenticated response with Vary: Authorization?

It is technically possible in some systems, but it is a high-risk design. Prefer private or an explicit tenant-public representation. If a shared cache is unavoidable, prove the key, authorization independence, purge, and cross-tenant isolation with integration tests and operational controls.

Public sources

Related questions