Skip to content

19 Caching Design

DodaTech 3 min read

title: Caching Design in REST APIs — Complete Guide weight: 29 date: 2026-06-28 lastmod: 2026-06-28 description: Learn REST API caching with ETags, Cache-Control headers, conditional requests, and server-side caching strategies to reduce latency and server load. tags: [api-development, rest]


Caching in REST APIs reduces server load and response latency by storing and reusing previous responses, using Cache-Control headers for freshness, ETags for validation, and conditional requests for efficient revalidation.

```mermaid
flowchart TD
  A[Client] --> B{Has cached?}
  B -->|No| C[Server]
  C --> D[Response + Cache-Control]
  D --> E[Store in cache]
  B -->|Yes, but stale| F[Conditional GET]
  F --> G{Modified?}
  G -->|Yes| H[200 + new data]
  G -->|No| I[304 Not Modified]
  style A fill:#e1f5fe
  style C fill:#f3e5f5
  style I fill:#c8e6c9

Cache-Control headers tell clients and intermediaries how long to cache a response. max-age=3600 means cache for one hour. ETags are unique identifiers for resource versions. Clients send If-None-Match with the ETag. If the resource has not changed, the server returns 304 Not Modified with no body.

Think of caching like a library book. The Cache-Control header is the due date (return by Friday). The ETag is the edition number (3rd edition). If you bring the book back and ask for the same edition (If-None-Match), the librarian says "still the same" (304) rather than handing you the identical book again.

Example: Cache-Control Headers

import requests

response = requests.get("https://api.example.com/users/42")
print(f"Cache-Control: {response.headers.get('Cache-Control')}")
print(f"ETag: {response.headers.get('ETag')}")
print(f"Expires: {response.headers.get('Expires')}")

Expected output:

Cache-Control: public, max-age=3600, must-revalidate
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Expires: Mon, 29 Jun 2026 10:00:00 GMT

Example: Conditional Request with ETag

import requests

# First request: get the ETag
response = requests.get("https://api.example.com/users/42")
etag = response.headers.get("ETag")
print(f"Initial ETag: {etag}")

# Second request: conditional with If-None-Match
headers = {"If-None-Match": etag}
response = requests.get(
    "https://api.example.com/users/42",
    headers=headers
)
print(f"Conditional status: {response.status_code}")
print(f"Body present: {'data' in response.text}")

Expected output:

Initial ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Conditional status: 304
Body present: False

Example: Last-Modified with Conditional Request

import requests
from datetime import datetime

# First request: get Last-Modified
response = requests.get("https://api.example.com/users/42")
last_modified = response.headers.get("Last-Modified")
print(f"Last modified: {last_modified}")

# Conditional request
headers = {"If-Modified-Since": last_modified}
cached_response = requests.get(
    "https://api.example.com/users/42",
    headers=headers
)
print(f"Conditional status: {cached_response.status_code}")

# Resource changed, get new version
headers = {"If-Modified-Since": "Mon, 01 Jan 2024 00:00:00 GMT"}
new_response = requests.get(
    "https://api.example.com/users/42",
    headers=headers
)
print(f"Updated status: {new_response.status_code}")

Expected output:

Last modified: Mon, 29 Jun 2026 08:00:00 GMT
Conditional status: 304
Updated status: 200

Common Mistakes

  1. Not setting Cache-Control headers — Without caching headers, clients and proxies may cache or not cache unpredictably, leading to stale data or unnecessary requests.
  2. Using Pragma: no-cache instead of Cache-Control — Pragma is a legacy HTTP/1.0 header. Use Cache-Control for modern caching control.
  3. Caching authenticated responses globally — Responses with user-specific data should use private or no-store to prevent mixing user data.
  4. Setting max-age too high — Long cache times improve performance but serve stale data. Balance freshness with performance based on your data update frequency.
  5. Not revalidating after mutations — After a PUT, PATCH, or DELETE, invalidate the cached response so the next GET returns fresh data.

Practice Questions

  1. What is the difference between public and private Cache-Control directives?
  2. What does the ETag header represent?
  3. How does a conditional GET with If-None-Match work?
  4. What status code is returned when a conditional GET finds no changes?
  5. Challenge: Implement a caching proxy in Python that stores GET responses, respects Cache-Control headers, performs conditional requests with ETags, and invalidates cache entries after mutations.

FAQ

What is the difference between Cache-Control: no-cache and no-store?

no-cache means the response must be revalidated with the server before use. no-store means the response must never be cached at all.

How long should I set max-age for different resource types?

Static resources: hours or days. User profiles: minutes. Real-time data: no-cache or very short max-age. Balance freshness with performance.

What is a cache key?

A cache key uniquely identifies a cached response, typically based on the request method, URI, and sometimes the Accept header.

Should I cache POST requests?

POST requests are rarely cached because they are not idempotent. Cache GET, HEAD, and sometimes responses to idempotent requests.

How do I handle caching in a clustered API deployment?

Use a distributed cache like Redis or Memcached that all API instances share. Invalidating cache in one instance should propagate to all.

Mini Project

Build a Python response caching system for a REST API client. The cache should store responses with ETags, support conditional requests for revalidation, respect Cache-Control max-age, and automatically invalidate entries after mutations.

What's Next

Now learn about rate limiting design in REST API Design.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro