Why this matters
As an API Engineer, you are responsible for fast, reliable, and cost-efficient APIs. Correct caching headers and ETags can cut latency, reduce server load, and improve user experience without changing business logic.
- Lower p95 latency by serving responses from browser or CDN caches.
- Slash origin traffic during spikes with shared-cache directives.
- Prevent stale or private data leaks by scoping cache correctly.
- Use conditional requests (ETag/If-None-Match) to avoid re-sending bodies when nothing changed.
Concept explained simply
Caching is a contract between your API and caches (browsers, CDNs, proxies). You tell caches how long to keep a response, who may store it, and how to revalidate it.
- Cache-Control: Core policy, e.g.,
max-age,s-maxage,public,private,no-cache,no-store,must-revalidate,stale-while-revalidate,stale-if-error. - ETag: A fingerprint of the response. Clients send it back via
If-None-Matchto ask, “Is this still the same?” If yes, server replies304 Not Modified. - Last-Modified: Timestamp of last change. Clients can revalidate with
If-Modified-Since. - Vary: Declares which request headers affect the response (e.g.,
Vary: Accept-Language). - Expires: Legacy absolute timestamp; typically use
Cache-Controlinstead.
Mental model
Think of cache policy as a recipe card:
- Who can keep it? public (any cache) or private (only browser).
- How long? seconds via
max-age(browser) ands-maxage(shared caches/CDNs). - What if it’s old? must-revalidate vs stale-while-revalidate vs stale-if-error.
- How to check freshness? Use ETag or Last-Modified for conditional GETs.
- Does it depend on request headers? Declare with
Vary.
Header cheat sheet (open)
Cache-Control: public, max-age=86400— Anyone can cache, for 1 day.Cache-Control: private, no-cache— Only end-user’s browser may store; must revalidate.Cache-Control: no-store— Do not store at all (sensitive data).s-maxage=...— Overrides max-age for shared caches (CDNs/proxies).stale-while-revalidate=...— Serve old content while fetching fresh in background.stale-if-error=...— Serve stale if origin fails.ETag: "abc123"— Strong validator; use withIf-None-Match.Last-Modified: Tue, 20 Jan 2026 10:00:00 GMT— Use withIf-Modified-Since.Vary: Accept, Accept-Encoding, Accept-Language— Response depends on these headers.
How to choose headers (step-by-step)
- Classify data sensitivity: If contains private or regulated data, prefer
privateorno-store. - Estimate change frequency: Rarely changes → longer
max-age; often changes → shortmax-age+ validators. - Decide cache scope: Public assets shared across users →
public. Per-user views →private. - Add validators: Provide
ETagand/orLast-Modifiedto enable 304 savings. - Tune shared caches: Use
s-maxage,stale-while-revalidate,stale-if-errorto improve CDN behavior. - Declare variability: If response changes by header (e.g., locale), set
Varyaccordingly. - Test with conditional requests: Confirm 200 with body on first request, then 304 on revalidation.
Worked examples
1) Static schema JSON (rare changes)
GET /openapi.json
200 OK
Cache-Control: public, max-age=604800, immutable
ETag: "f1b5c6"
Content-Type: application/json
{ ...large schema... }
Why: Large file, rarely changes. immutable says never revalidate within max-age.
2) Product catalog (updates hourly, shared via CDN)
GET /products?category=shoes
200 OK
Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=60, stale-if-error=86400
ETag: "p-cat-2026-01-21-10"
Last-Modified: Tue, 21 Jan 2026 10:00:00 GMT
Vary: Accept, Accept-Encoding
Content-Type: application/json
{ "items": [ ... ] }
Why: Browsers keep for 5 minutes; CDNs can keep for 1 hour. During refresh, CDN may serve stale for 60s. If origin fails, serve stale for a day. ETag/Last-Modified enable 304.
3) User dashboard (per-user, avoid shared cache)
GET /me/dashboard
200 OK
Cache-Control: private, no-cache, must-revalidate
ETag: "dash-u123-v7"
Vary: Accept, Accept-Encoding
Content-Type: application/json
{ ...private data... }
Why: Only the user’s browser may store it. no-cache forces revalidation each time; ETag enables 304 without re-sending the body.
4) Localized content (varies by language)
GET /articles/42 (Accept-Language: fr-FR)
200 OK
Cache-Control: public, max-age=86400
Vary: Accept-Language, Accept-Encoding
ETag: "art42-fr-v3"
Content-Type: application/json
{ ...texte en français... }
Why: Prevents English and French versions from overwriting each other in shared caches.
Exercises you can run today
These mirror the exercises below. Try first, then open the solution.
Exercise 1 — Catalog caching policy
Design headers for GET /catalog that updates daily, is safe to share via CDN, and should allow fast revalidation.
Expected behavior
- Browser can keep a short-lived copy.
- CDN can keep up to a day.
- Conditional GET returns 304 when unchanged.
Exercise 2 — Conditional GET with If-Modified-Since
Handle a request for /reports/monthly with If-Modified-Since. If data unchanged since the given date, reply with 304.
- Checklist
- Picked correct
Cache-Controlfor scope and freshness. - Included
ETagorLast-Modifiedconsistently on 200 and 304. - Used
Varyonly when needed. - Tested the 304 path with a second request.
- Picked correct
Common mistakes and self-check
- Forgetting validators: No ETag or Last-Modified means no 304 savings. Self-check: Do 200 and 304 both include validators?
- Overusing no-store: Blocks all caching, even harmless assets. Self-check: Could this be
privateorno-cacheinstead? - Missing Vary: Localized or compressed variants may collide. Self-check: Does response depend on
Accept*headers? - Public caching of private data: Leaks across users. Self-check: Any endpoint returning user-specific data must be
privateorno-store. - Not aligning CDN and browser TTLs: Only
max-ageused, but nos-maxage. Self-check: Are shared caches tuned withs-maxage? - Wrong 304 headers: 304 must not include a body but should include validators and cache headers. Self-check: Does your 304 return
ETag/Last-Modifiedand matchingCache-Control?
Who this is for
- API Engineers and Backend Developers building HTTP services.
- Engineers integrating CDNs or reverse proxies.
- Anyone troubleshooting slow endpoints or high origin load.
Prerequisites
- Comfortable with HTTP basics (methods, status codes, headers).
- Ability to run local server or API framework of your choice.
- Awareness of data sensitivity and privacy requirements.
Learning path
- Review HTTP caching semantics: freshness vs validation.
- Add
Cache-Controlto static-like endpoints. - Implement
ETaggeneration and conditional GET. - Introduce
s-maxageand stale directives for CDN paths. - Define Vary rules for content negotiation (locale, encoding, format).
- Set policies for private and sensitive endpoints.
- Measure: check hit ratios, latency, and origin load; adjust TTLs.
Practical projects
- Write a small middleware that computes strong ETags from response bodies (e.g., SHA-256 of JSON) and supports
If-None-Match. - Add
Last-Modifiedbased on database updated_at; respond 304 forIf-Modified-Since. - Apply
public, s-maxage, andstale-while-revalidatefor a read-heavy list endpoint behind a CDN; measure origin traffic reduction. - Audit endpoints to mark each as public, shared or private; propose specific headers for each.
Mini challenge
Pick one endpoint that currently has no caching headers. Propose a safe policy, add ETag or Last-Modified, and verify a 304 path with a second request. Aim for a 20% reduction in transferred bytes.
Next steps
- Implement validators on at least two endpoints.
- Tune CDN behavior with
s-maxageand stale directives where safe. - Document your cache matrix (endpoint → policy) for your team.
Quick Test
The Quick Test is available to everyone. Sign in to save your progress and track results over time.