In this tutorial, you'll learn about API Caching. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
API Caching uses HTTP headers, ETags, and CDNs to store responses and serve them without recomputation, reducing latency and server load while maintaining freshness.
What You'll Learn
You will learn how to configure HTTP cache headers, implement ETags for conditional requests, set up CDN Caching, and design cache invalidation strategies.
Why Caching Matters
Every uncached API request hits your database, consumes compute, and adds latency. Caching reduces response times from 200ms to under 10ms and cuts server costs by 70-90%. DodaTech's Durga Antivirus Pro threat intelligence API serves 50 million requests daily — Caching is what makes this sustainable.
HTTP Cache Headers
from Flask import Flask, jsonify, make_response
from datetime import datetime, timedelta
app = Flask(__name__)
THREATS_CACHE = {
"last_updated": "2026-06-24T10:00:00Z",
"threats": [
{"id": 1, "name": "Emotet", "severity": "high"},
{"id": 2, "name": "Mirai", "severity": "medium"},
],
}
@app.route("/v2/threats")
def list_threats():
response = make_response(jsonify(THREATS_CACHE))
# Cache for 5 minutes in browser, 10 minutes at CDN
response.headers["Cache-Control"] = "public, max-age=300, s-maxage=600"
# Vary based on Accept-Encoding to avoid serving gzipped to clients that don't support it
response.headers["Vary"] = "Accept-Encoding"
# Last-Modified for conditional requests
response.headers["Last-Modified"] = THREATS_CACHE["last_updated"]
return response
Expected response headers:
HTTP/1.1 200 OK
Cache-Control: public, max-age=300, s-maxage=600
Vary: Accept-Encoding
Last-Modified: 2026-06-24T10:00:00Z
ETags for Conditional Requests
ETags are unique identifiers for response content. Clients send If-None-Match to get a 304 Not Modified when the content has not changed.
import hashlib
import json
@app.route("/v2/threats/etag")
def list_threats_etag():
data = THREATS_CACHE["threats"]
# Generate ETag from MD5 hash of JSON body
body = json.dumps(data, sort_keys=True).encode()
etag = hashlib.md5(body).hexdigest()
# Check if client has the latest version
if_none_match = request.headers.get("If-None-Match")
if if_none_match == f'"{etag}"':
return "", 304
response = make_response(jsonify(data))
response.headers["ETag"] = f'"{etag}"'
response.headers["Cache-Control"] = "public, max-age=300"
return response
Expected output:
# First request — full response
curl -i http://localhost:5000/v2/threats/etag
# HTTP/1.1 200 OK
# ETag: "abc123def456"
# Content-Length: 142
# Second request — conditional, content unchanged
curl -i -H 'If-None-Match: "abc123def456"' http://localhost:5000/v2/threats/etag
# HTTP/1.1 304 Not Modified
# Content-Length: 0
CDN Caching with CloudFront / Cloudflare
@app.route("/v2/threats/feed")
def threat_feed():
response = make_response(jsonify(THREATS_CACHE))
# CDN should cache for 1 hour
response.headers["Cache-Control"] = "public, max-age=60, s-maxage=3600"
# CloudFront: forward only the needed query string params
response.headers["X-Cache-Config"] = "cdn-cache"
# Surrogate-Control for Akamai (ignored by browsers)
response.headers["Surrogate-Control"] = "max-age=3600"
return response
Expected CDN behavior:
# First request (cache MISS)
→ Origin server processes request, returns 200
→ CDN stores response for 3600 seconds
# Subsequent requests (cache HIT)
→ CDN returns cached response immediately
→ No request reaches origin
→ Response time: ~5ms instead of ~200ms
# After 3600 seconds (cache EXPIRED)
→ CDN revalidates with origin
→ If content unchanged: 304, CDN renews cache
flowchart LR
C["Client"] --> D["CDN Edge\nCloudFront/Cloudflare"]
D --> O["Origin Server\nFlask/Django"]
D --> CACHE["CDN Cache\ns-maxage=3600"]
C -->|"1. Request"| D
D -->|"2. Cache MISS"| O
O -->|"3. Response 200\n+ Cache Headers"| D
D -->|"4. Store & Forward"| C
C -->|"5. Repeat Request"| D
D -->|"6. Cache HIT"| CACHE
CACHE -->|"7. Fast Response"| C
style CACHE fill:#bbf7d0,stroke:#16a34a
style O fill:#dbeafe,stroke:#2563eb
style D fill:#fef3c7,stroke:#d97706
Cache Invalidation Strategies
import time
CACHE_BUSTER = int(time.time())
@app.route("/v2/threats/<threat_id>")
def get_threat(threat_id):
threat = fetch_threat(threat_id)
response = make_response(jsonify(threat))
# Short TTL for individual resources
response.headers["Cache-Control"] = "public, max-age=60"
return response
@app.route("/v2/admin/threats", methods=["POST"])
def create_threat():
data = request.get_JSON()
threat_id = insert_threat(data)
# Invalidate the list cache by bumping the buster
global CACHE_BUSTER
CACHE_BUSTER = int(time.time())
return jsonify({"id": threat_id}), 201
Expected behavior:
# After POST /v2/admin/threats:
# → List cache invalidated via CACHE_BUSTER
# → New threats appear in next GET /v2/threats
# → Individual threat is uncached (just created)
Common Errors
1. Over-Caching Dynamic Data
Setting max-age=86400 on user-specific data means users see stale information for a day. Use private or no-cache for personalized responses.
2. Not Setting Vary Headers
Without Vary: Accept-Encoding, a CDN may serve gzipped content to clients that do not support it. Always set appropriate Vary headers.
3. Cache Busting with Timestamps Only
Using only timestamps for cache busting under high load can cause thundering herd problems. Use a combination of version IDs and staggered TTLs.
4. Ignoring Cache on Error Responses
If your origin returns a 500 error, the CDN should not cache it. Set s-maxage=0 on error responses so clients retry the origin.
5. Stale-While-Revalidate Misuse
stale-while-revalidate serves stale content while fetching fresh data in the background. Use it for non-critical data, not for threat feeds where freshness is security-critical.
Practice Questions
1. What is the difference between max-age and s-maxage?
max-age applies to browser caches. s-maxage applies to shared caches like CDNs and proxies. When both are set, s-maxage overrides max-age for shared caches.
2. How does an ETag conditional request work?
The server generates a hash of the response content. The client sends If-None-Match with the previous ETag. If the content matches, the server returns 304 Not Modified with no body.
3. What does Cache-Control: private mean?
The response is specific to a user and must not be cached by shared caches (CDNs). Only the browser may cache it.
4. Challenge: Design a Caching Strategy for a threat feed that updates every 15 minutes, is consumed by 10,000 partners, and must never serve stale threat data for more than 5 minutes.
Set max-age=60, s-maxage=240 on the CDN. Use a background job that updates the feed every 15 minutes. On feed update, invalidate the CDN cache for the feed URL. Use stale-while-revalidate=120 as a safety net.
Mini Project: Cached Threat API
Build a Flask endpoint for Durga Antivirus Pro's threat list with ETag support, Cache-Control headers, CDN configuration, and a purge endpoint that admin can call to invalidate the cache after a threat database update.
Related Tutorials
RESTful API Design — API Gateway — Rate Limiting
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-24.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro