How to check if CloudFront is caching correctly
CloudFront adds an x-cache header to every response it serves. That header
is the ground truth: not your distribution settings, not what a plugin claims, but what
actually happened to this request at this edge location. You can read it with the checker
above, in your browser's DevTools (Network tab → select the request → Response Headers),
or with curl:
curl -sI https://example.com/ | grep -i -E 'x-cache|x-amz-cf-pop|age|cache-control'
A healthy cached response looks like this:
x-cache: Hit from cloudfront
x-amz-cf-pop: FRA56-P8
age: 1742
cache-control: public, max-age=86400
x-amz-cf-pop tells you which of CloudFront's 600+ points of presence
answered, and age how many seconds the object has been sitting in that
cache. Here is what each x-cache value means:
| x-cache value | What it means | Good or bad? |
|---|---|---|
Hit from cloudfront | Served straight from the edge cache; your origin was never contacted. | ✅ What you want |
Miss from cloudfront | Not in this location's cache. Fetched from the origin, then stored (if cacheable). | ⚠️ Slow for this visitor |
RefreshHit from cloudfront | The cached copy had expired; CloudFront revalidated it with the origin and the origin confirmed it's still fresh (304). | ✅ Cheap, but tune your TTLs |
Error from cloudfront | The request failed (origin error, timeout, or blocked). | ❌ Investigate |
No x-cache at all | The response didn't come through CloudFront. Check your DNS and distribution. | ❌ Nothing to hit |
"X-Cache: Miss from cloudfront" on every request? The three usual causes
1. Your origin forbids caching
CloudFront respects the Cache-Control header your origin sends. If your
server (or your framework, or a WordPress plugin) sends no-store,
private or max-age=0, CloudFront won't keep the object, and every
request stays a Miss. Check the response headers in the tool above: the fix is sending
Cache-Control: public, max-age=… from the origin, or setting minimum/default
TTLs in your cache policy.
One CloudFront quirk to know: if the origin sends no caching headers at all,
CloudFront caches for the default TTL (24 hours) anyway.
2. Your cache key is fragmented
CloudFront caches one copy per unique cache key. If your cache policy forwards
all query strings, all cookies, or headers like User-Agent, then
/page?utm_source=x and /page?utm_source=y are two separate cache
entries, and a session cookie makes every visitor's request unique. The result is a cache
that technically works but almost never hits. Forward only what actually changes the
response; start from the managed CachingOptimized policy and add exceptions
deliberately.
3. The cache is per location, and most locations are cold
This is the one that surprises people: a Hit in Frankfurt says nothing about Singapore. Each CloudFront point of presence keeps its own cache (with regional edge caches as a middle tier), and an object is only stored where someone has requested it. Low-traffic pages also get evicted long before their TTL runs out. In our measurement of 408,000 CDN requests, a miss was 3.5× slower than a hit at the median. That's exactly why the checker above tests from six locations at once.
Checking your CloudFront cache hit ratio
For the aggregate view, open the AWS console → CloudFront → your distribution → Reports & analytics → Cache statistics. The "percentage of viewer requests by result type" chart is your hit ratio over time. As a rule of thumb: static assets should sit at 95%+, and a well-configured site can reach 85–95% for HTML. If your ratio is much lower, work through the three causes above in order; cache-key fragmentation is the most common culprit on real-world distributions.
Clearing the CloudFront cache (invalidations)
To purge cached objects, create an invalidation on the distribution
(console → Invalidations → Create, or aws cloudfront create-invalidation
--distribution-id ID --paths "/*"). Two things worth knowing:
- Costs: the first 1,000 invalidation paths per month are free, after
that $0.005 per path. A wildcard like
/*counts as a single path. - For assets, version instead of invalidating: content-hashed
filenames (
app.a1b2c3.js) with long TTLs never need invalidation. Reserve invalidations for HTML and other stable URLs.
And remember what an invalidation does globally: it empties the cache in every edge location at once. Right after a deploy plus invalidation, your whole edge is cold and every first visitor per location pays the full origin round trip.
From checking to fixing: keep the cache warm
Checking tells you where the cache is cold. Warming fixes it: requesting your pages
through CloudFront from many regions on a schedule, so each edge location has already
pulled and stored the object before a real visitor arrives. warmup.rocks does that from
42 countries, verifies the x-cache header of every response, and re-warms
automatically after your deploy hook fires, which is the natural companion to an
invalidation.
FAQ
What does "X-Cache: Miss from cloudfront" mean?
The requested object wasn't in the cache of the edge location that served you, so CloudFront fetched it from your origin. If the response was cacheable, it's stored now and the next request to the same location is a Hit. If you see Miss on every request, your origin headers or cache policy usually prevent caching; see the three causes above.
Why is it a Hit from one location and a Miss from another?
CloudFront caches per point of presence. Each of the 600+ locations keeps its own independent cache, filled only by requests that arrive there. Traffic from Europe warms European PoPs and does nothing for São Paulo or Tokyo. Multi-location checks (or the Cache statistics report) reveal this; a single curl from your own machine can't.
Does an invalidation clear all edge locations?
Yes. An invalidation removes the matching objects from every CloudFront edge location worldwide. That's the point, but it also means the entire edge is cold afterwards. If you invalidate on every deploy, consider warming right after, so visitors don't pay for your deploy schedule.
How do I increase my CloudFront cache hit ratio?
In order of impact: send explicit Cache-Control: public, max-age=…
from the origin, trim the cache key (only forward query strings, cookies and headers
that change the response), lengthen TTLs so objects survive between visits, and warm
the long tail of pages that organic traffic alone doesn't keep resident, especially
outside your main market.
Is this checker free? Does it store my URLs?
Yes, it's free and requires no signup. Checks run live and results aren't stored; we keep a short-lived, in-memory rate-limit counter per IP and nothing else.
Stop checking. Start warming.
warmup.rocks requests your pages from 90+ CDN edge locations on a schedule and shows you the hit ratio per location, per run, for CloudFront, Cloudflare, Fastly and more.
Start your 7-day free trial