Most explanations of how a CDN works stop at the edge. A reader in Frankfurt gets your file from a server in Frankfurt instead of your origin in Virginia, and the origin is spared. That picture assumes one cache. A real CDN runs hundreds of independent caches, and each one that misses asks your origin for the same object again. So the load and the egress bill you see depend on how many edges miss, and on what sits between them and your server.
This page explains edge caching, the cache key and hit ratio, and the extra tier that CloudFront calls Origin Shield, Cloudflare calls Tiered Cache and Fastly calls shielding. The top results we checked were vendor glossaries that measured nothing, so we built a simulation: four nginx edge caches, one nginx shield and an origin that logs every fetch, under 30,000 requests. The behavior described here was checked against nginx 1.24 on Ubuntu 24.04 in November 2026, and every price comes from the vendor pricing page that same week.
How a CDN works when a request reaches the edge
When you request a file through a CDN, DNS or anycast routing sends you to a nearby point of presence (PoP). The cache in that PoP decides one thing first: does it hold a fresh copy of the object under this cache key? If it does, the response leaves the edge without touching your servers. If it doesn’t, the edge becomes a client of the next hop, fetches the object, stores it if the response headers allow, and answers you. Everything else on this page is a variation of that one decision.
Every cache reports how that decision went, and nginx exposes it as $upstream_cache_status. Commercial CDNs use the same vocabulary in headers such as X-Cache or cf-cache-status, so these are worth knowing by name:
| Status | What the cache did |
|---|---|
| HIT | Served a fresh stored copy; no upstream request was made |
| MISS | Had nothing usable, fetched upstream and stored the reply if it was cacheable |
| EXPIRED, REVALIDATED | Held a copy past its TTL and asked upstream again; on REVALIDATED the origin only confirmed the copy was still good |
| STALE, UPDATING | Served an old copy on purpose, because the origin failed or a refresh was already in flight |
| BYPASS | Skipped the cache for this request because a bypass rule matched |
You can watch those states change on any cache. In the lab, three header requests went out for the same object: two to edge PoP 1 on port 8091 and one to edge PoP 2 on port 8092, with both edges pointed at the shield. Point the same loop at your own edge and shield ports to check a setup of yours. The origin side of this lab, an nginx on 8080 in front of a small test app, is not reproduced here:
for p in 8091 8091 8092; do curl -sI http://127.0.0.1:$p/obj/42.bin | grep -iE "^(HTTP|cache-control|x-)"; echo; done > curl-headers.txt
cat curl-headers.txt
Both tiers miss on the first request. The second is an edge HIT. The third is the interesting one: PoP 2 had never seen the object, so it missed. The shield already held it and answered with a HIT, so the origin was not asked a second time.

One detail in that output caught us out. The second response still says X-Shield-Cache: MISS, although the shield did no work for it. The edge stored the shield’s headers along with the body, so it replays the shield status from the moment the object was filled. When you debug a tiered CDN from headers alone, read the status from the upper tier as “what happened at fill time”, and read the logs when you need the truth for a single request.
Logs settle it. Each tier writes the port, path, status, bytes, cache status and request time, and the origin log has a line only when something reached it:

Three edge lines, two shield lines and a single origin line, taking 51 ms because the test origin adds 50 ms per request. That ratio of one origin fetch per object is the whole argument for a shield, and the rest of the lab measures how far it holds under load.
What decides your CDN cache hit ratio
Cache hit ratio is the share of requests an edge answers from its own cache. Most of what sets it is in your hands. The headers your origin sends decide whether a response may be stored and for how long (Cache-Control: public, max-age=300 in the lab, s-maxage if you want shared caches to keep it longer than browsers do). The cache key decides which requests count as “the same object”. Your traffic shape decides how often popular objects are asked for again. What you mostly don’t control is how the CDN spreads users across PoPs, and the lab shows that it matters more than most explanations admit.
The cache key is where good hit ratios quietly die. A key built from the full request URI will treat /obj/7.bin and /obj/7.bin?utm_id=123 as two objects, so every tracking parameter, cache buster or session token in a query string creates a new miss. We ran the same 30,000-request mix twice with 30% of requests carrying a unique ?utm_id=, first with proxy_cache_key $request_uri, then with $uri:

With the query string in the key, the edge hit ratio fell from 82.7% to 55.4% and origin requests rose from 5,185 to 13,371, which is 2.6 times the origin load for identical content. That’s why every CDN offers a way to drop or allow-list query parameters in the cache key, and why it’s the first setting we would check on a CDN bill that looks too high. We did not measure the shield in this test. On a CDN where every tier uses the same cache-key policy, which is the usual setup, a fragmented key fragments the shield as well, so a shield is no fix for it.
How origin shield and tiered cache work
An origin shield is a cache placed between the edge PoPs and your origin, so that every edge miss goes to the shield first and only the shield’s own misses reach your server. Without one, a new object requested in 40 PoPs can be fetched from the origin 40 times. With one, it is fetched once and the other 39 edges fill from the shield. In order:
- The user’s request lands on the nearest edge PoP, which answers from cache on a hit.
- On a miss, the edge asks the shield instead of the origin.
- The shield answers from its own cache if any edge has asked for that object before, and only goes to the origin on its own miss.
That path, as the lab built it, uses the same ports and cache names as every test below:

“Origin shield” and “tiered cache” describe the same mechanism, and the difference between vendors is how many upper tiers there are and who picks them. CloudFront already sends edge misses through regional edge caches, and Origin Shield adds one more layer in an AWS Region you choose, which AWS says lets it fetch each object “with a single origin request”. Cloudflare’s Tiered Cache picks the upper tier for you with Smart Topology, while its generic global, regional and custom topologies are Enterprise features. Fastly calls it shielding and lets you choose the shield PoP per backend. The table puts the names, the billing and the plan limits side by side:
| CDN | Feature name | Who picks the upper tier | How it is billed |
|---|---|---|---|
| Amazon CloudFront | Origin Shield (on top of regional edge caches) | You pick the AWS Region | $0.0075 per 10,000 shield requests (US), $0.0090 (Europe) |
| Cloudflare | Tiered Cache, Smart Topology | Cloudflare, from latency data | Docs list Smart Topology on Free, Pro, Business and Enterprise; the plans page now sells it inside Smart Shield from $5/month |
| Fastly | Shielding | You pick a shield PoP | Traffic into the shield is billed as regular requests and bandwidth |
| Google Cloud CDN | No separately named shield feature | Not documented | Cache fill per GiB: $0.01 within North America or Europe, $0.04 between regions |
| Bunny CDN | Origin Shield | You pick the shield region per Pull Zone | No extra charge, and shield-to-edge traffic is not billed |
For Cloudflare, the row needs the caveat it carries. The Tiered Cache documentation, as we read it in November 2026, still lists Smart Topology as available on every plan, Free included. The same page now carries a banner moving the feature into Smart Shield, and the Cloudflare Free plan page prices “Smart Shield + Argo Smart Routing” from $5 a month. If you need the answer for your own zone, the Caching section of the dashboard shows whether Tiered Cache is on.
What the lab measured with and without a shield
This lab simulates tiering on one VM. It is not a CDN, and it measures request counts rather than network latency, because all the tiers sit on the same host. Four nginx server blocks on ports 8091 to 8094 play edge PoPs, each with its own cache zone. A fifth block on 8081 plays the shield, and an origin nginx on 8080 logs every request it receives before passing it to a small app that adds 50 ms per response. A Python driver sends 30,000 requests from a seeded Zipf distribution over 2,000 objects of 100 KB, spread at random across the edges. A few objects are very popular and most are rarely asked for, which is how real asset traffic behaves.
The shield is an ordinary nginx cache with two settings that do the real work. proxy_cache_lock lets only one request at a time fill a missing object, so a burst of misses becomes one upstream fetch. proxy_cache_use_stale updating keeps serving the old copy while one request refreshes it. An origin can ask any shared cache for that same behavior with the stale-while-revalidate and stale-if-error directives of RFC 5861. The lock has a limit: after proxy_cache_lock_timeout (5 seconds by default) the waiting requests go upstream anyway and their responses are not cached. A very slow origin still sees a stampede. This is the exact file the lab ran:
sudo vim /etc/nginx/conf.d/20-shield.conf
Its contents, with the shield listening on 8081 and pointing at the origin on 8080:
# shield: one mid-tier cache in front of the origin
proxy_cache_path /var/cache/nginx/shield levels=1:2 keys_zone=shield:20m max_size=2g inactive=1h use_temp_path=off;
server {
listen 127.0.0.1:8081;
access_log /var/log/nginx/shield.log cdnlog;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_cache shield;
proxy_cache_key $uri;
proxy_cache_lock on;
proxy_cache_use_stale updating error timeout;
proxy_cache_background_update on;
add_header X-Shield-Cache $upstream_cache_status;
}
}
Each edge is the same pattern with its own cache zone. Below is the first of the four edge blocks from the shielded run, opened the same way:
sudo vim /etc/nginx/conf.d/00-edges.conf
The log_format line lives here because nginx reads this file first, and the other three edges differ only in port and zone name:
log_format cdnlog '$server_port $uri $status $body_bytes_sent $upstream_cache_status $request_time';
proxy_cache_path /var/cache/nginx/edge1 levels=1:2 keys_zone=edge1:10m max_size=1g inactive=1h use_temp_path=off;
server {
listen 127.0.0.1:8091;
access_log /var/log/nginx/edge.log cdnlog;
location / {
proxy_pass http://127.0.0.1:8081;
proxy_cache edge1;
proxy_cache_key $uri;
proxy_cache_lock on;
proxy_cache_use_stale updating error timeout;
add_header X-Edge-Cache $upstream_cache_status;
}
}
For the no-shield runs, proxy_pass points at 127.0.0.1:8080 instead, and nothing else changes. A wrapper script wipes every cache and log before each run, so each one starts cold. This run has the edges fetching straight from the origin:

Edges answered 82.7% of requests from cache, which sounds healthy, yet the origin still served 5,189 requests and 531 MB. Only 1,911 distinct objects were ever requested, so the origin delivered most of them more than twice, once for each PoP that happened to see them. The shielded run uses the same seeded request sequence:

The edge hit ratio didn’t move, because a shield changes nothing at the edge. What changed is behind it: the shield absorbed 3,277 of the 5,189 edge misses (a 63% shield hit ratio), and origin requests fell to 1,912, which is the 1,911 distinct objects plus the one object from the burst test. Origin traffic dropped from 531 MB to 196 MB, 63% less for the same user-facing result. Counted against all 30,200 requests, including the burst, the share answered without the origin went from 82.8% to 93.7%.
The PoP count is what makes the shield pay. We repeated the run spread across one, two, three and four edges, and the chart shows the origin fetches for each:

With one PoP the shield adds nothing, since there is nobody to share a fill with. Without a shield, each extra PoP added between 850 and 1,400 origin fetches, while the shielded line stays flat at the number of distinct objects. The edge hit ratio also falls from 93.6% to 82.7% as traffic spreads across more PoPs, because each cache sees a smaller slice of the requests. That’s the trade every CDN makes: more PoPs put copies closer to users and lower the hit ratio of each, and a shield is how you stop that dilution from reaching your origin. The raw numbers behind the chart:

Bursts are the second job a shield does. Each run started by sending 200 simultaneous requests, 50 per edge, for one object nobody had cached, while the origin took one second to answer. Our first attempt at this test showed nothing, because the nginx limit_rate directive could not slow a 100 KB response on loopback (the socket buffer swallowed it at once), so every request after the first found a finished copy. Moving the delay into the origin app fixed it, and with the cache lock turned off at the edges the result was the stampede you’d expect:

All 200 requests reached the origin. Turning the lock on cut that to 4, one per edge, and adding the shield cut it to 1, with or without the edge lock, because the shield’s own lock collapsed the 200 requests that arrived there. The chart puts the four configurations together:

This is the case AWS describes as consolidating “simultaneous requests” at Origin Shield, and it matters more than the steady-state numbers when a release, a newsletter or a purge sends everyone to an uncached URL at once. If your origin generates content per request, such as image resizing or on-the-fly packaging, a shield turns a stampede into a single job.
How hit ratio changes the bill for 10 TB a month
A CDN bill has four parts that move differently with hit ratio: edge egress to users, per-request fees, shield fees, and the egress your origin pays to fill the CDN. We priced a two-tier CDN, edge plus an optional shield like the lab, at CloudFront pay-as-you-go US list rates, because CloudFront publishes all four. The volume is 10 TB a month at 80, 90 and 95% edge hit ratio, with 100 KB per response, which makes 100 million requests. The origin is assumed to sit outside AWS and pay Google Cloud Premium Tier internet egress, and the shield columns use the 63% shield hit ratio from the lab.

Rework the same numbers with your own rates from this table:
| Edge hit ratio | Edge egress | Request fees | Shield fees | Origin egress (no shield / shield) | Total (no shield / shield) |
|---|---|---|---|---|---|
| 80% | $765.00 | $90.00 | $15.00 | $215.01 / $82.24 | $1,070.01 / $952.24 |
| 90% | $765.00 | $90.00 | $7.50 | $111.64 / $41.06 | $966.64 / $903.56 |
| 95% | $765.00 | $90.00 | $3.75 | $55.76 / $20.47 | $910.76 / $879.22 |
Treat the shield saving as a ceiling for CloudFront itself. Its regional edge caches already absorb part of the edge misses. A real no-shield CloudFront bill therefore has less origin egress than the model shows.
AWS also charges Origin Shield only for cacheable misses that arrive from a regional edge cache in a different Region, while the model charges every edge miss. One AWS rule cuts the other way: GET and HEAD requests with a TTL under 3,600 seconds count as dynamic, and Origin Shield always charges for those, which would include the lab’s own max-age=300 objects.
Edge egress is 71 to 87% of every bill, and hit ratio doesn’t touch it, because users receive 10 TB whatever happens behind the edge. Hit ratio and the shield only move the smaller blocks: going from 80 to 95% saves $159 a month without a shield, and the shield saves $118 at 80% but only $32 at 95%. The shield fee itself is small, $15 at most, so a shield pays for itself when your origin pays for egress and objects are large. At a 63% shield hit ratio and about $0.11/GiB origin egress, the break-even sits near a 12 KB average response. Below that, the request fee costs more than the egress it saves. The picture flips for an origin inside AWS, since CloudFront states that origin fetches from AWS services are free of charge. There the shield costs $3.75 to $15 a month and saves no egress at all, and you would turn it on for origin load, not for the bill. Our notes on pairing a CDN with cloud services cover that origin placement question in more depth.
How CDN egress pricing compares per TB
Per-TB price varies by more than ten times between CDNs that publish a rate card, so the cheapest lever for most sites is the provider, not the hit ratio. The table lists the first-party list price for North America and Europe and what the same 10 TB and 100 million requests would cost at a 90% hit ratio on each. Anything not on the vendor’s own page is marked as such rather than estimated.
| CDN | Edge egress, NA/EU | Request fees | 10 TB + 100M requests at 90% hit |
|---|---|---|---|
| Bunny CDN, Standard Network | $0.01/GB ($10/TB), $1 monthly minimum | None | $100.00 |
| Google Cloud CDN | $0.08/GiB up to 10 TiB | $0.0075 per 10,000 lookups, plus $0.01/GiB cache fill | $829.37 |
| Amazon CloudFront, pay-as-you-go | First 1 TB free, then $0.085/GB for the next 9 TB | First 10M free, then $0.0100 per 10,000 HTTPS (US) | $855.00, plus $7.50 with Origin Shield |
| Fastly Full Site Delivery | First 100 GB free, then $0.12/GB to 10 TB | First 1M free, then $0.01 per 10,000 | $1,287.00 |
| Cloudflare | Plan price, no per-GB line on the plans page | None listed | Free $0, Pro $20/month and Business $200/month billed annually |
| Akamai | No public rate card | Quote only | Not published |
| Gcore CDN | 1 TB free each month, no public per-GB rate found | Quote only | Not published |
Read the table as list prices, not quotes. Contract pricing is negotiated separately at most of these vendors, the Fastly pricing page also sells monthly packages from $1,500, and every rate here was read in early November 2026 and will change. The Google figure assumes the origin fills caches in its own region and leaves out the Cloud Load Balancing charges that Cloud CDN runs behind. The useful comparison is structural: Bunny charges for bytes only, the hyperscalers charge for bytes and requests, and Cloudflare charges for the plan. If you serve millions of small responses, the request line can matter as much as the per-GB rate. Running two of these at once is common too, and our piece on CDN traffic steering covers how traffic gets split between them.
When an origin shield does not help
A shield saves origin work only when two or more edges would otherwise miss on the same object, and several common setups never meet that condition. The AWS documentation lists dynamic content proxied to the origin, content with low cacheability and content that is requested infrequently as cases where Origin Shield “may not be a good fit”. The lab adds the single-PoP case, and together they look like this:
- Uncacheable APIs and personalised pages: every request goes to the origin anyway, so a shield only adds a hop and, on CloudFront, a fee per request.
- Traffic concentrated in one region: with one busy PoP, the lab measured identical origin fetches with and without the shield (1,911 each).
- Long-tail content: objects requested once a month expire from both tiers before anyone asks again, so the shield never gets its second request.
- A fragmented cache key: on a CDN that applies one key policy to every tier, unique query strings fragment the shield just as they fragment the edge.
For everything else, fix the cache key and the Cache-Control headers first, because they raise the edge hit ratio for free, then add the shield where your origin is expensive to reach or expensive to run. If the origin itself is the bottleneck, load balancing a high-traffic website is the other half of the answer, since a shield reduces how often the origin is asked but not how much each request costs it.
Tools in this space
Below are the CDNs and caching layers referenced above, listed alphabetically and described only by what the vendors’ documentation and pricing pages state. We have not benchmarked them against each other, so nothing here is a ranking.
- Akamai: a large commercial CDN whose delivery products are sold through its sales team; akamai.com/pricing leads to cloud computing prices, not a per-GB CDN rate.
- Amazon CloudFront: AWS’s CDN, with regional edge caches as a built-in mid tier and Origin Shield as an optional extra layer billed per request.
- Bunny CDN: a pay-as-you-go CDN billed per GB by region on a Standard Network and a lower-cost Volume Network, with no request fees and an Origin Shield at no extra cost.
- Cloudflare: a plan-priced CDN whose Tiered Cache picks an upper tier automatically with Smart Topology; other topologies are Enterprise-only.
- Fastly: a CDN with usage pricing per GB and per request, where shielding sends edge misses through a shield PoP you choose.
- Gcore CDN: a CDN that advertises 1 TB of free traffic a month on its product page; larger volumes are quoted.
- Google Cloud CDN: the CDN behind Google Cloud load balancers, billed for cache egress, cache fill and cache lookups.
- nginx and Varnish: open-source caches you can run as your own edge or shield tier, the way the lab does; our guide to putting Varnish in front of Apache or nginx shows the single-tier version.
Video delivery has its own criteria beyond caching and cost, and our roundup of CDNs for video streaming covers those. For live video, our guide to low-latency live streaming compares WebRTC, LL-HLS and RTMP.
Sources
Every lab number above comes from runs on one Ubuntu 24.04 VM with nginx 1.24 and Python 3.12 in November 2026. Prices and plan details were read from these first-party pages on 2 November 2026:
- nginx proxy module,
proxy_cache_lock,proxy_cache_use_staleandproxy_cache_background_update: nginx.org;$upstream_cache_statusvalues: nginx upstream module - HTTP caching, including
s-maxage: RFC 9111;stale-while-revalidateandstale-if-error: RFC 5861 - CloudFront Origin Shield behaviour, fit and how its charges are estimated: AWS documentation; rates and free origin fetches from AWS: CloudFront pay-as-you-go pricing
- Cloudflare Tiered Cache plan availability: Cloudflare docs; plan prices: cloudflare.com/plans and the Free plan page
- Fastly rates: fastly.com/pricing (Full Site Delivery); shield billing: Fastly shielding guide
- Bunny rates: bunny.net/pricing/cdn; Bunny Origin Shield: bunny.net docs; Google Cloud CDN rates: cloud.google.com/cdn/pricing; origin egress used in the model: Google Cloud network pricing (Premium Tier to North America)
- Gcore free allowance: gcore.com/cdn; Akamai pricing page: akamai.com/pricing
The cost model assumes decimal units (10 TB as 10,000 GB), converts to GiB where a vendor bills per GiB, and treats every response as 100 KB, so byte and request hit ratios are equal. Real traffic mixes small and large objects, and a byte hit ratio usually sits above the request hit ratio when large files are the popular ones, so rerun the numbers with the analytics from your own CDN before you change providers.