REST Caching Strategies — Cache-Control and ETag for API Performance
In this tutorial, you will learn about REST Caching Strategies. We cover key concepts, practical examples, and best practices to help you master this topic.
REST caching uses Cache-Control headers, ETags, and conditional requests to allow clients and intermediaries to cache responses, reducing server load and improving API response times.
What You'll Learn
- How Cache-Control directives control caching behavior
- How ETags enable conditional requests
- How to implement caching for different resource types
Why It Matters
Caching is the single most effective performance optimization for APIs. A well-cached API serves 80%+ of requests from cache, reducing server load by 5x and response times from 200ms to under 10ms. Without caching, every request hits your database.
Real-World Use
DodaTech's threat intelligence API uses aggressive caching. Public threat feeds are cached for 5 minutes with public Cache-Control. User-specific threat queries use ETags for conditional requests. 75% of requests are served from cache, reducing database queries from 10,000/s to 2,500/s.
flowchart LR
A["Client Request"] --> B{"Has valid\ncache?"}
B -->|"Yes (fresh)"| C["Return cached\nresponse\n~5ms"]
B -->|"No (stale)"| D["Send request\nto server"]
D --> E{"Resource\nchanged?"}
E -->|"No (304)"| F["Return cached\n+ new headers"]
E -->|"Yes (200)"| G["Return new\nresponse"]
style C fill:#bbf7d0,stroke:#16a34a
style F fill:#fef3c7,stroke:#d97706
style G fill:#dbeafe,stroke:#2563eb
Cache-Control Directives
from flask import Flask, jsonify, make_response, request
import hashlib
import json
from datetime import datetime, timedelta
app = Flask(__name__)
# Public cacheable resource
@app.route('/api/public/threat-feeds')
def get_threat_feeds():
feeds = get_feeds()
response = make_response(jsonify(feeds))
# Cache for 5 minutes in public caches (CDN, proxy)
response.headers['Cache-Control'] = 'public, max-age=300'
response.headers['ETag'] = calculate_etag(feeds)
response.headers['Expires'] = (
datetime.utcnow() + timedelta(seconds=300)
).strftime('%a, %d %b %Y %H:%M:%S GMT')
return response
# Private resource (user-specific)
@app.route('/api/user/profile')
def get_user_profile():
user_id = get_authenticated_user()
profile = get_profile(user_id)
response = make_response(jsonify(profile))
# Private: only the user's browser should cache this
response.headers['Cache-Control'] = 'private, max-age=60'
return response
# No cache (sensitive data)
@app.route('/api/auth/token', methods=['POST'])
def get_token():
response = make_response(jsonify({"token": "..."}))
response.headers['Cache-Control'] = 'no-store'
return response
ETag and Conditional Requests
def calculate_etag(data):
"""Generate ETag from response data"""
serialized = json.dumps(data, sort_keys=True)
return hashlib.md5(serialized.encode()).hexdigest()
@app.route('/api/products/<int:product_id>')
def get_product(product_id):
product = get_product_by_id(product_id)
current_etag = calculate_etag(product)
# Check If-None-Match header
if_none_match = request.headers.get('If-None-Match')
if if_none_match == current_etag:
# Resource hasn't changed
return '', 304
response = make_response(jsonify(product))
response.headers['ETag'] = current_etag
response.headers['Cache-Control'] = 'public, max-age=60'
return response
@app.route('/api/products/<int:product_id>', methods=['PUT'])
def update_product(product_id):
data = request.json
updated_product = update_product_in_db(product_id, data)
# The ETag changes after update
new_etag = calculate_etag(updated_product)
response = make_response(jsonify(updated_product))
response.headers['ETag'] = new_etag
return response
Last-Modified Headers
@app.route('/api/articles/<int:article_id>')
def get_article(article_id):
article = get_article_by_id(article_id)
last_modified = article.updated_at
# Check If-Modified-Since header
if_modified_since = request.headers.get('If-Modified-Since')
if if_modified_since:
client_time = datetime.fromisoformat(if_modified_since.replace('GMT', '').strip())
if last_modified <= client_time:
return '', 304
response = make_response(jsonify(article.to_dict()))
response.headers['Last-Modified'] = last_modified.strftime('%a, %d %b %Y %H:%M:%S GMT')
response.headers['Cache-Control'] = 'public, max-age=300'
return response
Common Mistakes
1. Not Setting Cache-Control at All
Without Cache-Control, clients and proxies guess caching behavior. Always set explicit Cache-Control headers.
2. Caching Authenticated Responses as Public
User-specific data (profile, orders) must be private to prevent one user's data being served to another.
3. Using Weak ETags When Strong Ones Are Needed
A strong ETag changes when the content changes down to the byte. A weak ETag (prefixed with W/) changes when the meaning changes. Use strong ETags for byte-range requests.
4. Forgetting to Invalidate Cache on Updates
When a resource is updated, its ETag changes. But cached copies at CDNs may still serve stale data. Use purge APIs or short TTLs.
5. Not Varying Cache by Accept-Encoding
If you compress responses, cache must vary by Accept-Encoding or serve different versions for compressed vs uncompressed.
Practice Questions
- What is the difference between public and private Cache-Control?
- How do ETags enable conditional requests?
- What status code indicates the resource hasn't changed?
- When would you use no-store vs no-cache?
- How do you validate that caching works correctly?
Answers
- Public caches (CDNs/proxies) can cache public data; private data only caches in the user's browser. 2. Clients send If-None-Match with the previous ETag; server returns 304 if unchanged. 3. 304 Not Modified. 4. no-store: never cache; no-cache: check with server before using cached version. 5. Check response headers and verify 304 responses for conditional requests.
Challenge
Build a caching layer for a blog API that: uses ETags for all resources, sets appropriate Cache-Control for public (articles) and private (user drafts) resources, implements conditional requests, and includes cache invalidation when resources are updated.
FAQ
Mini Project
Build a REST API with a comprehensive caching Strategy: public resources (catalog, articles) with 5-minute TTL, private resources (user data) with ETag validation, no-store for auth endpoints, and a cache hit-rate monitoring endpoint that tracks cache performance.
What's Next
- Learn about the layered system constraint with proxies and gateways
- Explore code-on-demand as an optional REST constraint
- Continue to resource naming conventions for consistent Api Design
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro