Cloud

How a CDN Works: Edge Caching, Origin Shield and Cost per TB

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.

Original content from computingforgeeks.com - post 182646

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:

StatusWhat the cache did
HITServed a fresh stored copy; no upstream request was made
MISSHad nothing usable, fetched upstream and stored the reply if it was cacheable
EXPIRED, REVALIDATEDHeld a copy past its TTL and asked upstream again; on REVALIDATED the origin only confirmed the copy was still good
STALE, UPDATINGServed an old copy on purpose, because the origin failed or a refresh was already in flight
BYPASSSkipped 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.

curl output showing X-Edge-Cache and X-Shield-Cache headers changing from MISS to HIT across two CDN edge PoPs

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:

nginx edge, shield and origin access logs showing upstream_cache_status MISS and HIT for one object

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:

CDN cache hit ratio falling from 82.7 to 55.4 percent when the cache key includes a unique query string

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:

  1. The user’s request lands on the nearest edge PoP, which answers from cache on a hit.
  2. On a miss, the edge asks the shield instead of the origin.
  3. 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:

CDN origin shield architecture diagram: users to four nginx edge PoPs on ports 8091 to 8094, edge misses to one shield on 8081, shield misses to the origin on 8080

“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:

CDNFeature nameWho picks the upper tierHow it is billed
Amazon CloudFrontOrigin Shield (on top of regional edge caches)You pick the AWS Region$0.0075 per 10,000 shield requests (US), $0.0090 (Europe)
CloudflareTiered Cache, Smart TopologyCloudflare, from latency dataDocs list Smart Topology on Free, Pro, Business and Enterprise; the plans page now sells it inside Smart Shield from $5/month
FastlyShieldingYou pick a shield PoPTraffic into the shield is billed as regular requests and bandwidth
Google Cloud CDNNo separately named shield featureNot documentedCache fill per GiB: $0.01 within North America or Europe, $0.04 between regions
Bunny CDNOrigin ShieldYou pick the shield region per Pull ZoneNo 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:

Lab run without origin shield: 82.7 percent edge hit ratio and 5,189 origin requests for 30,000 CDN requests

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:

Lab run with origin shield: same 82.7 percent edge hit ratio, origin requests down to 1,912 and shield hits 3,277

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:

Chart of CDN origin fetches by number of edge PoPs: 1,911 to 5,185 without a shield, flat at 1,911 with an origin shield

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:

Terminal output of origin requests per PoP count with and without a CDN origin shield

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:

Cold burst with no cache lock and no shield: all 200 CDN requests miss and 200 reach the origin

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:

Chart of origin fetches for 200 simultaneous CDN requests: 200 with no cache lock, 4 with lock, 1 with an origin shield

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.

Stacked chart of two-tier CDN cost per 10 TB at CloudFront list rates for 80, 90 and 95 percent hit ratio, split into edge egress, request fees, shield fees and origin egress

Rework the same numbers with your own rates from this table:

Edge hit ratioEdge egressRequest feesShield feesOrigin 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.

CDNEdge egress, NA/EURequest fees10 TB + 100M requests at 90% hit
Bunny CDN, Standard Network$0.01/GB ($10/TB), $1 monthly minimumNone$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-goFirst 1 TB free, then $0.085/GB for the next 9 TBFirst 10M free, then $0.0100 per 10,000 HTTPS (US)$855.00, plus $7.50 with Origin Shield
Fastly Full Site DeliveryFirst 100 GB free, then $0.12/GB to 10 TBFirst 1M free, then $0.01 per 10,000$1,287.00
CloudflarePlan price, no per-GB line on the plans pageNone listedFree $0, Pro $20/month and Business $200/month billed annually
AkamaiNo public rate cardQuote onlyNot published
Gcore CDN1 TB free each month, no public per-GB rate foundQuote onlyNot 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:

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.

Keep reading

Free CCNA 200-301 Practice Questions with Answers Networking Free CCNA 200-301 Practice Questions with Answers Configure Samba File Share on Debian 13 / 12 Debian Configure Samba File Share on Debian 13 / 12 WireGuard VPN Setup: Ubuntu 26.04/24.04, Debian 13, Rocky 10 Debian WireGuard VPN Setup: Ubuntu 26.04/24.04, Debian 13, Rocky 10 Homelab and Sysadmin Gifts by Budget: $25, $50, $100 and $250+ Networking Homelab and Sysadmin Gifts by Budget: $25, $50, $100 and $250+ Best AWS Solutions Architect Associate Books for SAA-C03 (No SAA-C04 Yet) Books Best AWS Solutions Architect Associate Books for SAA-C03 (No SAA-C04 Yet) Wireless Networking Fundamentals: Channels, SSID, Security Networking Wireless Networking Fundamentals: Channels, SSID, Security

Leave a Comment

Press ESC to close