Caching Pitfalls for Real-Time Data Behind an Edge CDN
A CDN is built to reuse responses. Real-time data, such as prices, order books, balances or positions, is only correct for a very short time and is often specific to one user. Putting both behind the same distribution works, but only if every real-time path is explicitly excluded from caching. Most problems come from defaults that nobody looked at.
Pitfall 1: no Cache-Control means the CDN decides
If the origin sends no Cache-Control or Expires, the CDN applies its own default TTL. On CloudFront without a cache policy, that default is 24 hours. The managed CachingOptimized policy has a minimum TTL of 1 second, a default of 86,400 seconds and a maximum of one year. A JSON endpoint that forgot to set headers behind that policy is cached for a day.
Cloudflare by default caches by file extension and does not cache JSON or HTML, but a "cache everything" rule changes that for every path it matches.
Pitfall 2: minimum TTL overrides no-store
On CloudFront, if the cache behavior's minimum TTL is greater than 0, CloudFront caches the object for at least that long even when the origin sends Cache-Control: no-cache, no-store or private. The AWS documentation states this explicitly. So "the API sends no-store" is not enough if the path falls under a behavior with CachingOptimized. Use the managed CachingDisabled policy (all TTLs 0) for real-time paths.
Pitfall 3: the wrong behavior matches
Path patterns are evaluated in order, and anything that does not match a specific pattern falls through to the default behavior. A new endpoint under /v2/ while the no-cache behavior only covers /api/* lands in the cached default. Keep the default behavior safe for dynamic content, or make sure every API prefix has its own behavior, and review this whenever routes change.
Pitfall 4: a cache key that ignores the user
If a response depends on a cookie or the Authorization header and that value is not part of the cache key, the first user's response is served to everyone else with the same URL. Web cache deception attacks exploit a related gap: a path like /account/settings/x.css is cached because of its extension, while the origin ignores the suffix and returns private data. Do not try to solve personalised responses with cache keys and Vary. Keep them out of the shared cache entirely.
Pitfall 5: stale content on origin errors
stale-if-error and stale-while-revalidate are good for product pages and bad for prices. During an origin outage they make the CDN serve old data that looks current. CloudFront has a related default: if the origin is unreachable and the minimum or maximum TTL is greater than 0, it may serve the object it fetched earlier. The documented way to prevent that is sending Cache-Control: stale-if-error=0.
Pitfall 6: purge as a correctness tool
Invalidation is asynchronous and takes time to reach every edge. It is fine for fixing a wrong image. It is not a way to keep financial data correct. If correctness depends on a purge finishing quickly, the data should not be cached.
Pitfall 7: streaming through the edge
WebSocket and Server-Sent Events connections pass through most CDNs without caching, but they are subject to idle timeouts and, for SSE, possible response buffering. Send heartbeats more often than the idle timeout, check your provider's documented limits, and test reconnect behavior under load.
How to set it up
Classify every path. Static assets, public data that changes slowly (instrument metadata, trading calendars), public data that changes fast (a public ticker snapshot), and private data. Only the first two belong in the cache by default.
Send explicit headers from the origin for every response:
# private or real-time
Cache-Control: no-store
# public snapshot that may be reused for one second at the edge only
Cache-Control: public, max-age=0, s-maxage=1, stale-if-error=0
Some CDNs also support CDN-Cache-Control (RFC 9213), which targets CDN caches without affecting browsers. Even then, keep Cache-Control correct, because it is the header every cache understands.
Exclude real-time paths in the CDN config, not only in headers. Two layers mean one mistake does not leak data. In Terraform for CloudFront:
data "aws_cloudfront_cache_policy" "disabled" {
name = "Managed-CachingDisabled"
}
data "aws_cloudfront_origin_request_policy" "all_viewer" {
name = "Managed-AllViewerExceptHostHeader"
}
resource "aws_cloudfront_distribution" "app" {
# origins, default_cache_behavior, viewer_certificate and the rest omitted
ordered_cache_behavior {
path_pattern = "/api/*"
target_origin_id = "api"
viewer_protocol_policy = "https-only"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
cached_methods = ["GET", "HEAD"]
cache_policy_id = data.aws_cloudfront_cache_policy.disabled.id
origin_request_policy_id = data.aws_cloudfront_origin_request_policy.all_viewer.id
}
}
Cache policies are separate resources referenced by ID. There is no cache_policy block inside aws_cloudfront_distribution. On Cloudflare, the equivalent is a Cache Rule that bypasses cache for the API paths.
Put freshness in the payload. Include a server timestamp and a sequence number in every real-time message. Clients compare the timestamp with their own clock and discard or flag data older than the threshold your product defines. This catches stale data from any source: CDN, proxy, browser cache or a stuck backend.
Plan for the CDN being unavailable. If the real-time feed is only reachable through the CDN, a CDN incident means no data. Decide whether clients should fail over to a direct origin hostname, and test it.
Verify continuously
The response headers tell you what the cache did. Age greater than 0 means the response came from a cache. CloudFront adds X-Cache, Cloudflare adds CF-Cache-Status.
for i in 1 2 3; do
curl -s -o /dev/null -D - https://www.example.com/api/v1/quotes/ABC \
| grep -i -E '^(age|cache-control|x-cache|cf-cache-status):'
sleep 1
done
For a real-time path you expect Miss from cloudfront or DYNAMIC/BYPASS, and no Age header. Run this as a synthetic check against every real-time endpoint and alert when a Hit shows up. Track hit ratio per path in CDN logs too, since a rising hit ratio on an API path is a warning, not a success.
Checklist
- Explicit
Cache-Controlon every origin response,no-storefor private and real-time data. CachingDisabled(or a bypass rule) on all real-time paths, and a safe default behavior.- No minimum TTL above 0 where the origin relies on
no-store. - No
stale-if-erroron prices;stale-if-error=0where the CDN would otherwise serve old objects. - Personalised responses never in the shared cache.
- Timestamps and sequence numbers in payloads, checked by clients.
- Synthetic checks that alert on cache hits for real-time endpoints.
