Skip to content
Home
API Caching: Redis, CDN, and Response Caching

API Caching: Redis, CDN, and Response Caching

API Development API Development 8 min read 1492 words Beginner ExcellentWiki Editorial Team

Caching is the single most effective performance optimization for APIs. A well-cached API reduces database load, lowers latency, and handles traffic spikes without scaling. Every layer of the stack — from the CDN at the edge to the in-memory cache beside the database — can be tuned for caching.

Caching Layers

API responses can be cached at multiple levels:

LayerLocationLatencyStorageInvalidation
CDNEdge nodes1–10 msDistributedTTL + purge API
Reverse proxyNginx/HAProxy<1 msRAM on proxyTTL + config reload
Application cacheRedis/Memcached<1 msRemote RAMTTL + explicit delete
In-process cacheApplication heap<0.1 msLocal RAMTTL + eviction policy
Database cacheBuffer pool<10 msRAM + diskBuilt-in

Each layer has trade-offs. CDN caches are great for public, cacheable responses but cannot serve authenticated content. In-process caches are blazing fast but do not scale across instances. Redis is the sweet spot for most APIs: fast, shared across all application instances, and supports rich invalidation patterns.

Cache Headers

HTTP cache headers control how responses are cached at the CDN, proxy, and browser levels:

Cache-Control: public, max-age=3600, s-maxage=3600
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Tue, 15 Nov 2024 12:45:26 GMT
Expires: Tue, 15 Nov 2024 13:45:26 GMT
Vary: Accept-Encoding
  • Cache-Control — primary directive; public allows any cache, private restricts to browser cache, no-cache requires revalidation
  • max-age — seconds the response is considered fresh; s-maxage overrides for shared (proxy/CDN) caches
  • ETag — opaque hash of the response content; clients send If-None-Match for validation
  • Last-Modified — timestamp of last change; clients send If-Modified-Since for validation
  • Vary — which request headers affect the cached representation (typically Accept-Encoding or Accept-Language)

Invalidating CDN caches is more expensive than application cache invalidation. A common strategy is to use short max-age (5–60 seconds) on the CDN and rely on the application cache for longer-lived storage with explicit invalidation.

ETags and Conditional Requests

ETags enable lightweight validation caching. When a client sends If-None-Match with the previous ETag, the server can return 304 Not Modified with an empty body:

from hashlib import sha256

def get_user(request, user_id):
    user = get_user_from_db(user_id)
    etag = sha256(json.dumps(user).encode()).hexdigest()

    if request.headers.get("If-None-Match") == etag:
        return Response(status=304)

    return Response(json=user, headers={"ETag": etag})

Strong ETags (byte-exact) are preferred for API responses. Weak ETags (W/"...") are acceptable for large responses where byte equality is too expensive to compute. The 304 Not Modified response saves bandwidth — the client already has the content in its local cache — but the server still computes the ETag, so the CPU savings depend on how expensive the ETag computation is versus full response generation.

For expensive API calls, compute the ETag from a lightweight checksum (like a hash of the database row’s updated_at timestamp) instead of hashing the entire response payload.

Redis Caching Patterns

Redis is the most common application-side cache for APIs. Several patterns address different needs:

Read-Through Cache

def get_product(product_id):
    cache_key = f"product:{product_id}"
    cached = redis.get(cache_key)
    if cached:
        return json.loads(cached)
    product = db.query(Product).get(product_id)
    redis.setex(cache_key, 300, json.dumps(product.to_dict()))
    return product

Cache-Aside (Lazy Loading)

The application checks the cache first, and on a miss, loads from the database and populates the cache. This is the most common pattern. The TTL is the safety net — even if the application fails to invalidate explicitly, the cache will eventually expire.

Write-Through Cache

def update_product(product_id, data):
    product = db.query(Product).get(product_id)
    product.update(data)
    db.commit()
    cache_key = f"product:{product_id}"
    redis.setex(cache_key, 300, json.dumps(product.to_dict()))
    return product

Cache Invalidation Strategies

StrategyMechanismProsCons
TTLTime-based expirySimple, eventually consistentStale data until expiry
Write-throughUpdate cache on writeAlways freshHigher write latency
Cache evictionDelete key on writeLow write overheadCache miss on next read
Pub/SubBroadcast invalidate across instancesConsistent across workersInfrastructure complexity

A common production pattern is to delete the cache key on writes (cache eviction) and rely on TTL as a fallback. This avoids the write amplification of write-through while keeping reads fresh on the next request. For high-traffic endpoints, a write-through pattern may be preferable to avoid stampedes (multiple concurrent requests all triggering a cache miss and hitting the database simultaneously).

CDN Caching

CDNs cache at the HTTP level and respect Cache-Control headers. For API responses:

  • Public endpoints (product listings, blog posts): cache at the CDN with s-maxage=60 — the CDN serves most requests, the application serves only misses
  • Authenticated endpoints: set Cache-Control: private — CDNs must not cache responses with user-specific content
  • Soft purges: update cached content without invalidating the CDN URL (useful for correcting stale data)
  • Stale-while-revalidate: serve stale content while refreshing in the background — Cache-Control: stale-while-revalidate=30 means serve stale for up to 30 seconds while fetching fresh
Cache-Control: public, max-age=10, stale-while-revalidate=30

This pattern is especially valuable for APIs that cannot tolerate cache misses: the CDN serves stale data for up to 30 seconds while it fetches a fresh response from the origin, so the client never sees a cache miss.

Cache Stampede Prevention

A cache stampede (thundering herd) happens when a popular cache key expires and thousands of requests hit the database simultaneously before any of them writes the new cache value.

Solutions:

  • Locking: Only one request computes the value; others wait (Redis SETNX with lock key, or a mutex in the application)
  • Early recalculation: Refresh the cache before it expires if the value is near TTL and the key is hot
  • Probabilistic early expiration: Randomly refresh early based on remaining TTL — Netflix’s approach
  • Permanent cache + background refresh: Cache never expires; a background job refreshes it periodically
import random

def get_hot_product(product_id):
    cache_key = f"product:{product_id}"
    cached = redis.get(cache_key)
    if cached:
        ttl = redis.ttl(cache_key)
        product = json.loads(cached)
        # Probabilistic early recomputation — 10% chance when TTL < 60s
        if ttl < 60 and random.random() < 0.1:
            spawn_background_refresh(product_id, cache_key)
        return product
    return recompute_product(product_id, cache_key)

Cache Granularity

GranularityExampleProsCons
Full responseEntire /productsSimple, fastStale across users
ResourceSingle product objectTargeted invalidationMany keys
Database querySQL result setWorks with any dataHard to invalidate
FragmentHTML snippetFine-grainedComplexity

Most APIs should start with resource-level caching (cache per object) and add full-response caching for endpoints with simple invalidation rules. Avoid caching entire collections when individual items change independently — you will either serve stale collections or invalidate the entire collection cache on every item update.

Redis Memory Management

Redis stores data in memory, so cache size directly impacts infrastructure cost. Set a maximum memory limit (maxmemory) and configure an eviction policy for when that limit is reached. The allkeys-lru policy evicts the least recently used keys, which works well for most API caches where older data is less likely to be requested. For time-series data or event caches, volatile-ttl evicts keys with the shortest remaining TTL first. Monitor Redis memory usage and set alerts at 70% and 85% capacity. If your cache consistently exceeds 80% of maxmemory, scale up the Redis instance or add a secondary cache layer. Use redis-cli --bigkeys periodically to identify oversized keys that may indicate caching granularity problems.

FAQ

What is the difference between a cache hit and a cache miss? A cache hit occurs when the requested data is found in the cache, allowing the API to return it immediately without querying the database. A cache miss occurs when the data is not in the cache, requiring a full database query and then populating the cache for future requests.

How long should cache TTL be? It depends on your data’s freshness requirements. For real-time data, TTL of 1-5 seconds is appropriate. For product catalogs, 5-60 minutes. For static reference data, hours or days. Start with short TTLs and increase them as you validate cache correctness.

What is the best caching strategy for authenticated APIs? Set Cache-Control: private and use ETags for validation. The browser will cache responses per-user. For API-to-API communication, implement application-level caching with user-specific cache keys.

How do I handle caching with paginated results? Cache each page independently with its own key. Include the page number and page size in the cache key. When any item on a page changes, invalidate only that page. Consider cursor-based pagination for more stable cache keys.

Can I cache POST requests? HTTP allows caching POST responses, but most CDNs and proxies do not implement it. For POST endpoints, implement application-level caching based on the request body hash as part of the cache key. Ensure idempotency before caching POST responses.

How do I handle cache warm-up after a deployment? Implement a cache warm-up script that runs as part of your deployment pipeline. Identify the most frequently accessed keys from production traffic patterns (e.g., top 1000 product IDs) and pre-populate the cache before routing traffic to the new instances. Use a staggered deployment strategy — warm caches on a subset of instances before routing traffic, then warm caches on remaining instances. For critical endpoints, consider maintaining a hot standby cache that persists across deployments using Redis persistence (RDB snapshots or AOF logs) so the cache is not cold after a restart.

Internal Links

REST API Design GuideAPI Security Best PracticesAPI Monitoring Tools

Section: API Development 1492 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top