Debugging CloudFront cache misses for images
A CloudFront image cache hit ratio below 90% is almost always a configuration problem, not a traffic problem — your images should be near-static, and every miss re-fetches from the origin (or re-runs a transcode), inflating latency and cost. This guide, part of AWS CloudFront Cache Behaviors for Media inside CDN & Edge Media Delivery, gives you a repeatable procedure: read three response headers to localize the miss, isolate which cache-key dimension is fragmenting the object, confirm the scope in CloudWatch, and apply the matching fix. The causes are finite and each has a clean remedy.
The three headers that localize a miss
Every CloudFront response carries diagnostic headers. Three of them answer “did this hit, how old is it, and which edge answered?”
x-cache—Hit from cloudfront(served from the POP’s cache),Miss from cloudfront(went to origin),RefreshHit from cloudfront(revalidated a stale object), orError from cloudfront.age— seconds the object has lived in this POP’s cache.age: 0on a supposed Hit means it was just (re)fetched. A risingageacross repeat requests confirms a stable cached object.x-amz-cf-pop— the edge location code (e.g.LHR50-C1). CloudFront caches per POP, so a Miss simply because your two requests hit different POPs is not a bug.
# Baseline probe: repeat the SAME request twice and read the diagnostic trio.
for i in 1 2; do
curl -sI -H 'Accept: image/avif,image/webp,*/*;q=0.8' \
https://d111111abcdef8.cloudfront.net/images/hero.jpg \
| grep -iE 'x-cache|^age|x-amz-cf-pop|content-type|cache-control'
echo "--- request $i ---"
done
# Healthy result: request 1 = Miss (age 0), request 2 = Hit (age rising), SAME pop.
# If request 2 is still a Miss on the same POP, the object is not being cached.
Prerequisite checklist
Causes and fixes
1. Query strings in the cache key
If the image behavior’s Cache Policy sets QueryStringBehavior: all (or the legacy ForwardedValues forwards all query strings), then hero.jpg?v=1, hero.jpg?utm_source=x, and hero.jpg are three different cache objects. Analytics and cache-busting params multiply this endlessly.
# Prove it: the SAME image with two different junk query strings should NOT
# create two objects. If each is a fresh Miss, query strings are keying the cache.
curl -sI 'https://d111111abcdef8.cloudfront.net/images/hero.jpg?utm=a' | grep -i x-cache
curl -sI 'https://d111111abcdef8.cloudfront.net/images/hero.jpg?utm=b' | grep -i x-cache
Fix: Set QueryStringBehavior: none on the media Cache Policy. If you genuinely resize by query (?w=800), whitelist only the real sizing parameters and nothing else.
2. Cookies forwarded into the key
A Cache Policy with CookieBehavior: all fragments the cache per unique cookie value — meaning per logged-in user, per session, per A/B bucket. Images become effectively uncacheable.
Fix: Set CookieBehavior: none on the media behavior. Media never needs cookies; if the origin requires one for auth, forward it via the Origin Request Policy without keying it.
3. Accept over-keying
Adding Accept to the cache key is required for AVIF/WebP negotiation — but keying the raw Accept string fragments the cache across every browser build’s slightly different header. You get correct formats and a terrible hit ratio simultaneously.
# Two real-world Accept strings that select the same format should hit the same object.
# If both Miss, you are keying the raw string, not a normalized token.
curl -sI -H 'Accept: image/avif,image/webp,image/apng,*/*;q=0.8' \
https://d111111abcdef8.cloudfront.net/images/hero.jpg | grep -i x-cache
curl -sI -H 'Accept: image/avif,image/webp,*/*' \
https://d111111abcdef8.cloudfront.net/images/hero.jpg | grep -i x-cache
Fix: Normalize Accept to image/avif/image/webp/image/jpeg in a viewer-request CloudFront Function so at most three variants exist.
4. TTL too short (or origin no-cache)
If the origin sends Cache-Control: max-age=60 — or nothing, so DefaultTTL is a low value — objects expire before the next viewer arrives, especially on low-traffic POPs. RefreshHit in x-cache with frequent revalidation is the tell.
Fix: Serve content-hashed URLs with Cache-Control: public, max-age=31536000, immutable from the origin, per best practices for setting max-age on CDN media assets. Confirm the origin is not sending no-cache/private, which suppress caching unless MinTTL overrides them.
5. Per-POP cold cache (a non-problem)
CloudFront has 600+ POPs and each caches independently. An image requested only once an hour, spread across many POPs, will show frequent Misses that are expected — the object simply ages out or was never warm at that edge. This is not a misconfiguration; it is the nature of a distributed cache with low request density.
Fix: Nothing to fix per se, but you can raise the effective hit ratio for infrequently requested assets by enabling Origin Shield (a designated mid-tier cache region) so POP misses collapse to a single shielded origin fetch instead of hitting your origin from every edge.
Confirm scope with CloudWatch and logs
Header probing tells you about one URL; CloudWatch tells you about all of them. The CacheHitRate metric (percentage of viewer requests served from the edge) is the headline number.
# Pull the last 24h of CacheHitRate for the distribution (metrics live in us-east-1).
aws cloudwatch get-metric-statistics --region us-east-1 \
--namespace AWS/CloudFront --metric-name CacheHitRate \
--dimensions Name=DistributionId,Value=E1EXAMPLE2ID Name=Region,Value=Global \
--start-time "$(date -u -d '24 hours ago' +%FT%TZ)" \
--end-time "$(date -u +%FT%TZ)" \
--period 3600 --statistics Average --output table
To find which images miss, query the standard access logs (the x-edge-result-type field records Hit, Miss, RefreshHit, LimitExceeded, etc.):
# With logs in Athena, rank the worst-caching image URIs by miss count.
# x-edge-result-type = Miss isolates true origin fetches; sc_content_type filters to images.
# SELECT cs_uri_stem, count(*) AS misses
# FROM cloudfront_logs
# WHERE x_edge_result_type = 'Miss' AND sc_content_type LIKE 'image/%'
# GROUP BY cs_uri_stem ORDER BY misses DESC LIMIT 20;
A hit ratio that jumps immediately after tightening the Cache Policy confirms the fix; one that stays flat points at a genuinely sparse access pattern (cause 5) rather than key fragmentation.
Verification commands
# After the fix: the same URL should Hit on the second same-POP request, and two
# junk query strings / two equivalent Accept strings should share one object.
curl -sI 'https://d111111abcdef8.cloudfront.net/images/hero.jpg' | grep -iE 'x-cache|^age'
curl -sI 'https://d111111abcdef8.cloudfront.net/images/hero.jpg' | grep -iE 'x-cache|^age'
# Inspect the live Cache Policy to confirm cookies/query strings are 'none'
# and only Accept is whitelisted.
aws cloudfront get-cache-policy --id <cache-policy-id> \
--query 'CachePolicy.CachePolicyConfig.ParametersInCacheKeyAndForwardedToOrigin'
Common mistakes
1. Comparing Miss across different POPs
Anti-pattern: Concluding the cache is broken because two curls returned Miss — from two different x-amz-cf-pop values.
Effect: Wasted debugging; the per-POP cache is behaving correctly.
Fix: Always compare repeat requests that show the same x-amz-cf-pop. Pin to one POP (same network/region) when reproducing.
2. Reading age as a global TTL
Anti-pattern: Treating age as the object’s age across CloudFront.
Effect: Misdiagnosing a fresh object at a cold POP as “not caching.”
Fix: age is per-POP. Use CacheHitRate for the fleet-wide picture.
3. Fixing symptoms with invalidation
Anti-pattern: Running create-invalidation whenever an image looks stale or misses.
Effect: Wildcard invalidations throw away warm cache fleet-wide, spiking origin load and worsening the very hit ratio you are chasing.
Fix: Version URLs and correct the Cache Policy; reserve invalidation for emergencies, as covered in the parent guide.
Related
- AWS CloudFront Cache Behaviors for Media — cache key construction, TTL clamps, and invalidation vs versioned URLs
- CloudFront Cache Policy for Vary: Accept Negotiation — the Accept normalization that prevents key fragmentation
- Lambda@Edge AVIF Conversion on CloudFront — confirming an on-the-fly derivative actually caches
- Best Practices for Setting max-age on CDN Media Assets — the origin TTLs that keep objects warm