18 Pattern: Cache Control
The Cache Control pattern defines how APIs manage client-side and intermediary (CDN/proxy) caching using HTTP headers.
By leveraging response directives (Cache-Control) alongside conditional request headers (If-None-Match and If-Modified-Since), APIs allow clients to validate whether local content is still fresh—fetching full payload bodies only when resources have actually changed.
18.1. Overview
Caching is essential for building scalable, low-latency REST APIs. Rather than serving identical responses repeatedly, the Cache Control pattern establishes a contract between server, intermediaries, and client caches.
The pattern relies on two complementary mechanisms:
- Freshness Control (
Cache-Controlheader): Dictates where and for how long a response can be cached without checking back with the origin server (e.g.,max-age,public,private,no-cache,no-store). - Validation / Conditional Requests (
304 Not Modified): When cached data expires or requires revalidation, the client sends conditional headers (If-None-Matchwith anETag, orIf-Modified-Sincewith a timestamp). If unchanged, the server returns an empty304 Not Modifiedresponse, saving bandwidth and backend processing.
Note: Do not confuse read-side caching (
If-None-Match→304 Not Modified) with write-side concurrency control (If-Match→412 Precondition Failed). Read preconditions optimize fetching; write preconditions prevent lost updates.
18.2. When to Use Cache Control
Use Cache Control when:
- Serving static or slow-changing resources.
- e.g., Product catalogs, user profiles, public blog posts, asset metadata, or reference data.
- You need to reduce backend load and network bandwidth.
- Offloading repetitive
GETrequests to edge CDNs or browser caches dramatically reduces compute costs.
- Offloading repetitive
- Client applications require low latency.
- Serving data directly from local storage or edge locations yields sub-millisecond response times.
- Bandwidth is constrained (e.g., mobile clients).
- An empty
304 Not Modifiedheader-only response is significantly smaller than re-transmitting large JSON/XML payloads.
- An empty
18.3. When NOT to Use Cache Control
Avoid Cache Control (or enforce Cache-Control: no-store) when:
- Data is highly sensitive or strictly confidential.
- e.g., Banking transaction records, personal health information (PHI), or auth tokens where storing data in shared or disk-backed caches poses a compliance/security risk.
- Data changes continuously in real time.
- e.g., Financial market tickers, live GPS tracking, or high-frequency telemetry.
- Executing unsafe or state-changing operations.
- HTTP
POST,PUT,PATCH, andDELETErequests are inherently non-cacheable. Caching rules apply strictly to safe read operations (GET,HEAD).
- HTTP
18.4. What the Pattern Looks Like
Below are HTTP request and response interaction flows illustrating initial cache retrieval, conditional revalidation, and client recommendations.
1. Initial Retrieval (Fresh Response)
The client requests a resource for the first time. The server returns full representation with caching directives and validator headers (ETag / Last-Modified).
Request
GET /catalog/products/prd_991 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Response
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600, must-revalidate
ETag: "v1-8f2b1a"
Last-Modified: Mon, 03 Aug 2026 10:00:00 GMT
Content-Type: application/json
{
"id": "prd_991",
"name": "Wireless Mouse",
"price": 29.99
}
2. Conditional GET (Unchanged Resource → 304 Not Modified)
After 3600 seconds, the client’s local cache expires. The client revalidates by sending If-None-Match (or If-Modified-Since).
Request
GET /catalog/products/prd_991 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
If-None-Match: "v1-8f2b1a"
Response
The server computes or checks the current ETag. Because the resource has not changed, it returns 304 Not Modified without a body.
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=3600, must-revalidate
ETag: "v1-8f2b1a"
Last-Modified: Mon, 03 Aug 2026 10:00:00 GMT
(The client re-uses its locally cached representation for another 3600 seconds).
3. Conditional GET (Modified Resource → 200 OK)
If the product price changed on the backend, the server returns the updated representation with a new ETag.
Request
GET /catalog/products/prd_991 HTTP/1.1
Host: api.example.com
If-None-Match: "v1-8f2b1a"
Response
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600, must-revalidate
ETag: "v2-9x4c2b"
Last-Modified: Mon, 03 Aug 2026 14:15:00 GMT
Content-Type: application/json
{
"id": "prd_991",
"name": "Wireless Mouse",
"price": 24.99
}
Recommendations for Clients Using Cache Controls
To maximize API efficiency and avoid stale state bugs, client applications should adhere to these guidelines:
- Honor
Cache-ControlDirectives:- Do not issue network calls for cached entries while
max-ageis still fresh, unless forced by explicit user action (e.g., pull-to-refresh).
- Do not issue network calls for cached entries while
- Store Validators Alongside Payload:
- Always save response
ETagstrings andLast-Modifiedtimestamps locally alongside the response payload.
- Always save response
- Always Include Conditional Headers on Expiration:
- When re-fetching expired resources, populate
If-None-Matchwith the storedETag(and/orIf-Modified-Since).
- When re-fetching expired resources, populate
- Transparently Handle
304 Not Modified:- Treat a
304 Not Modifiedresponse as a success status: update local cache expiration timers and return the existing locally stored representation to application logic.
- Treat a
- Vary Cache Keys by Relevant Request Parameters:
- Ensure local cache stores key entries by full URL (including query parameters) and header dependencies specified in the server’s
Varyheader (e.g.,Accept-Language,Authorization).
- Ensure local cache stores key entries by full URL (including query parameters) and header dependencies specified in the server’s
18.5. Anti-Patterns to Avoid
1. Confusing no-cache with no-store
no-cache: Means “you may store this response, but you must revalidate it with the origin server before using it.”no-store: Means “do not store this response or request anywhere in any cache under any circumstances.”- Pitfall: Developers often use
no-cacheintending to disable caching completely for sensitive data, accidentally leaving copies stored on disk.
2. Omitting the Vary header on negotiated responses
HTTP/1.1 200 OK
Cache-Control: public, max-age=86400
/* Missing: Vary: Accept-Encoding, Accept-Language */
- If an endpoint returns localized content or compressed payloads (
gzip/brotli), omittingVary: Accept-Language, Accept-Encodingcauses CDNs to serve cached English/gzip content to Spanish/brotli clients.
3. Cache-busting with query strings instead of HTTP preconditions
GET /catalog/products/prd_991?_nocache=1722696000
- Forcing cache misses via random URL query string parameters bypasses CDN caches entirely, increasing server load and wasting network bandwidth. Use
Cache-Control: max-age=0orIf-None-Matchrevalidation instead.
4. Returning full payload bodies with 200 OK when preconditions match
- Returning a
200 OKwith the full JSON body when an incomingIf-None-Matchheader matches the currentETagdefeats the entire purpose of conditional requests. Compute or lookup the ETag early, and return an empty304 Not Modifiedimmediately.
18.6. OpenAPI Example
A complete OpenAPI 3.0.3 specification illustrating how to document caching headers (Cache-Control, ETag, Last-Modified, Vary), conditional query headers (If-None-Match, If-Modified-Since), and 304 Not Modified responses.
openapi: 3.0.3
info:
title: Product Catalog API - Cache Control Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/catalog/products/{productId}:
get:
summary: Retrieve product details with cache validation
description: Supports conditional GET requests using 'If-None-Match' or 'If-Modified-Since'. Returns 304 if unchanged.
tags: [Products]
parameters:
- in: path
name: productId
required: true
schema:
type: string
example: prd_991
- in: header
name: If-None-Match
required: false
schema:
type: string
example: '"v1-8f2b1a"'
description: Opaque ETag validator previously received from the server.
- in: header
name: If-Modified-Since
required: false
schema:
type: string
example: "Mon, 03 Aug 2026 10:00:00 GMT"
description: HTTP date string previously received in 'Last-Modified'.
responses:
'200':
description: Product retrieved successfully (full payload)
headers:
Cache-Control:
schema:
type: string
example: "public, max-age=3600, must-revalidate"
description: Directives for client and proxy caches.
ETag:
schema:
type: string
example: '"v1-8f2b1a"'
description: Entity tag validator for conditional revalidation.
Last-Modified:
schema:
type: string
example: "Mon, 03 Aug 2026 10:00:00 GMT"
description: HTTP date of the last modification.
Vary:
schema:
type: string
example: "Accept-Encoding, Authorization"
description: Headers influencing cache key generation.
content:
application/json:
schema:
$ref: '#/components/schemas/Product'
'304':
description: Not Modified - Cached representation is still fresh
headers:
Cache-Control:
schema:
type: string
example: "public, max-age=3600, must-revalidate"
ETag:
schema:
type: string
example: '"v1-8f2b1a"'
Last-Modified:
schema:
type: string
example: "Mon, 03 Aug 2026 10:00:00 GMT"
components:
schemas:
Product:
type: object
properties:
id:
type: string
example: prd_991
name:
type: string
example: Wireless Mouse
price:
type: number
format: float
example: 29.99
18.7. Visualizing Cache Control (Mermaid Diagram)
sequenceDiagram
autonumber
actor Client as Client App / Cache
participant CDN as Edge CDN / Proxy
participant API as API Server
participant DB as Database
Note over Client,DB: Phase 1: Initial Uncached Request
Client->>CDN: GET /catalog/products/prd_991
CDN->>API: GET /catalog/products/prd_991
API->>DB: Query product prd_991
DB-->>API: Return data
API-->>CDN: 200 OK (Cache-Control: max-age=3600, ETag: "v1-8f2b1a", Body)
CDN-->>Client: 200 OK (Cache-Control: max-age=3600, ETag: "v1-8f2b1a", Body)
Note over Client,CDN: Client & CDN store payload + ETag locally
Note over Client,CDN: Phase 2: Fresh Cache Window (t < 3600s)
Client->>Client: GET /catalog/products/prd_991
Note over Client: Served instantly from local cache without network call!
Note over Client,DB: Phase 3: Expired Cache Revalidation (Unchanged)
Client->>CDN: GET /catalog/products/prd_991 (If-None-Match: "v1-8f2b1a")
CDN->>API: GET /catalog/products/prd_991 (If-None-Match: "v1-8f2b1a")
API->>API: Compute ETag for prd_991 == "v1-8f2b1a"
API-->>CDN: 304 Not Modified (Header only, NO BODY)
CDN-->>Client: 304 Not Modified (Header only, NO BODY)
Note over Client: Client resets local max-age timer to 3600s
Note over Client,DB: Phase 4: Revalidation After Update
Client->>CDN: GET /catalog/products/prd_991 (If-None-Match: "v1-8f2b1a")
CDN->>API: GET /catalog/products/prd_991 (If-None-Match: "v1-8f2b1a")
API->>API: Compute ETag for prd_991 == "v2-9x4c2b" (Changed!)
API-->>CDN: 200 OK (ETag: "v2-9x4c2b", Updated Body)
CDN-->>Client: 200 OK (ETag: "v2-9x4c2b", Updated Body)
Note over Client: Client updates local cache with new body & ETag