19 Caching Design
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
- Not setting Cache-Control headers — Without caching headers, clients and proxies may cache or not cache unpredictably, leading to stale data or unnecessary requests.
- Using Pragma: no-cache instead of Cache-Control — Pragma is a legacy HTTP/1.0 header. Use Cache-Control for modern caching control.
- Caching authenticated responses globally — Responses with user-specific data should use
privateorno-storeto prevent mixing user data. - Setting max-age too high — Long cache times improve performance but serve stale data. Balance freshness with performance based on your data update frequency.
- Not revalidating after mutations — After a PUT, PATCH, or DELETE, invalidate the cached response so the next GET returns fresh data.
Practice Questions
- What is the difference between public and private Cache-Control directives?
- What does the ETag header represent?
- How does a conditional GET with If-None-Match work?
- What status code is returned when a conditional GET finds no changes?
- 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
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