Origin Shield Hit Ratio vs Purge Latency
A shield tier is a cache in front of your origin that every PoP misses into instead of hitting the origin directly. It buys origin offload measured in orders of magnitude, and it charges for that in one currency: a purge now has one more tier to clear before the edge can serve the new document.
For fingerprinted assets the trade is close to free — hashed files are never purged, so the shield’s hit ratio is pure profit. For the HTML entry points it is a real decision, because those are the objects a deploy invalidates, and their propagation time is now the sum of two tiers rather than one.
What the Shield Actually Changes
On an edge cache hit nothing changes at all: the response never leaves the PoP, and 98% or more of hashed-asset traffic lands there. The shield only participates on a miss, and that is where the two effects appear — a shorter fetch for most misses, a longer chain for every purge.
Comparing the Three Topologies
The numbers below assume a delivery profile of 20,000 requests per second at the edge, a 96% edge hit ratio, an origin in us-east-1, and user traffic spread across North America, Europe, and Asia-Pacific. Substitute your own edge hit ratio — the shape of the answer does not change, only the magnitudes.
| Dimension | No shield | Four regional shields | One global shield |
|---|---|---|---|
| Requests reaching the next tier | 800/s | 800/s | 800/s |
| Shield hit ratio | — | 80% | 85% |
| Origin requests per second | 800 | 160 | 120 |
| Median first byte, edge miss + tier hit | 142 ms | 30 ms | 24 ms (near) / 210 ms (far) |
| Median first byte, edge miss + tier miss | 142 ms | 154 ms | 166 ms |
| HTML purge propagation | ~0.9 s | ~1.3 s | ~1.4 s |
| Extra egress billed | none | shield → edge | shield → edge |
| Failure domain | one PoP | one region | everything |
| Best when | origin sits near your users | traffic is genuinely multi-region | origin is slow or metered hard |
The regional and global rows differ less in offload than people expect and more in tail latency. A single global shield aggregates every miss in the world into one cache, so it holds the longest tail of rarely requested chunks — but a Sydney PoP missing into a Virginia shield pays a 210 ms hop that a regional shield would have served in 30 ms. Cloudflare’s Tiered Cache exposes exactly this choice as Smart, Regional, and Generic Global topologies; the propagation behaviour of each is measured in tiered cache and hashed asset propagation.
Working the Hit-Ratio Arithmetic
Two caches in series multiply their miss ratios. That single fact is the whole model:
origin_rps = edge_rps × (1 − edge_hit_ratio) × (1 − shield_hit_ratio)
With edge_rps = 20000, edge_hit_ratio = 0.96, and no shield:
origin_rps = 20000 × 0.04 × 1.00 = 800
Add a shield holding 85% of what it is asked for:
origin_rps = 20000 × 0.04 × 0.15 = 120
The combined offload — the share of edge requests that never touch the origin — is 1 − (0.04 × 0.15) = 0.994, or 99.4%, against 96% with no shield. Framed as a hit ratio the improvement looks like rounding noise. Framed as origin load it is a 6.7× reduction, and that is the number your origin’s autoscaler, connection pool, and egress bill actually experience. Never evaluate a shield on the hit-ratio delta; evaluate it on the miss-count ratio.
The deploy-time picture is more dramatic, because a deploy is a synchronised cold-cache event across every PoP at once. A build emitting 300 new hashed chunks, delivered by two providers with roughly 40 active PoP regions each, produces up to 300 × 40 × 2 = 24,000 origin fetches as caches warm. One shared shield collapses that to 300 — every other request is served from a tier that already has the object.
Measure your own shield hit ratio from its access log rather than trusting a dashboard average, which usually blends HTML and assets into one meaningless number:
# Nginx shield: log_format must include $upstream_cache_status
# log_format shield '$request_uri $upstream_cache_status $request_time';
awk '$1 ~ /\/assets\// { total++; if ($2 == "HIT") hits++ }
END { printf "asset hit ratio: %.4f (%d/%d)\n", hits/total, hits, total }' \
/var/log/nginx/shield.log
awk '$1 !~ /\/assets\// { total++; if ($2 == "HIT") hits++ }
END { printf "html hit ratio: %.4f (%d/%d)\n", hits/total, hits, total }' \
/var/log/nginx/shield.log
# asset hit ratio: 0.9871 (394812/399978)
# html hit ratio: 0.4402 (12043/27358)
The split matters. An asset hit ratio of 98.7% at the shield means the shield is doing its job; an HTML hit ratio of 44% is normal and irrelevant, because HTML is short-lived by design. If your asset hit ratio at the shield is below about 95%, the shield is too small for your working set — raise max_size and inactive before concluding that shields do not help.
Why Regional and Global Shields Land on Different Ratios
The gap between 80% and 85% is not a vendor characteristic, it is arithmetic on request aggregation. A cache’s hit ratio over a large catalogue of rarely requested objects rises with the number of requests it sees per object per retention window. Split the same 800 misses per second across four regional shields and each one sees 200/s; a chunk requested twice a minute globally is requested once every two minutes at each regional shield, which is still inside a 30-day retention window but well outside the window for anything evicted under memory pressure.
Concretely, with four regional shields at 80%: 800 × 0.20 = 160 origin requests per second. With one global shield at 85%: 800 × 0.15 = 120. The global tier wins by 40 requests per second — about 25% less origin load — and loses roughly 180 ms of first-byte time for the PoPs furthest from it. If your origin is comfortable at 160 requests per second, take the regional topology and keep the latency. If it is not, the global shield buys you headroom that no amount of edge tuning will.
The Cost Side of the Ledger
Shield egress is billed twice for the same bytes: origin to shield, then shield to edge. At a typical object-storage egress rate and a 300 KB average chunk, the 300 fetches of a fresh deploy move about 90 MB from the bucket — negligible. Steady state is where it adds up: every edge miss that the shield serves is billed as shield egress instead of origin egress, and if you run the shield yourself on a compute instance you are also paying for the instance and its disk.
The comparison that matters is shield cost against origin cost at the traffic level you are actually removing. Removing 680 origin requests per second (800 down to 120) from a metered origin usually dwarfs the cost of one shield instance per region. Removing 40 requests per second does not. Compute the delta before choosing a topology, and re-compute it after your edge hit ratio improves — a shield sized for a 90% edge hit ratio is oversized once the edge reaches 98%.
Aligning Cache Keys Across Both Providers
A shield only reaches these hit ratios if both providers request the object identically. Two CDNs forwarding the same URL with different headers produce two shield cache entries for one file, halving the hit ratio and doubling the origin fetches — the failure looks like a shield that “does not work” and is actually a cache-key mismatch.
# Normalise everything the two providers might vary on before caching.
map $http_accept_encoding $norm_encoding {
default "";
"~*br" "br";
"~*gzip" "gzip";
}
server {
listen 443 ssl http2;
server_name shield.example.com;
# One entry per URI + negotiated encoding. Nothing provider-specific.
proxy_cache_key "$scheme$request_method$host$uri$norm_encoding";
# Strip headers that differ per provider and would otherwise reach the origin.
proxy_set_header CF-Connecting-IP "";
proxy_set_header Fastly-Client-IP "";
proxy_set_header X-Forwarded-For "";
proxy_set_header Accept-Encoding $norm_encoding;
# Refuse to let the origin fragment the cache further.
proxy_hide_header Vary;
add_header Vary "Accept-Encoding" always;
}
Two rules follow. Query strings must be excluded from the shield’s key for hashed assets — the filename already carries the version, so app.a1b2c3d4.js?v=2 and app.a1b2c3d4.js are the same bytes and must be the same entry. And Vary must be pinned to a value both providers can satisfy: a Vary: User-Agent leaking from the origin turns one cached chunk into thousands of near-duplicates and is the single fastest way to destroy a shield’s hit ratio.
What the Extra Hop Costs a Purge
Purge propagation is where the shield charges you. The edge cannot serve a new document until the shield has it, so total propagation is the shield’s clearing time plus the edge’s, not the maximum of the two.
Measure the real number for your topology rather than assuming the vendor’s published figure. This script purges a canary URL and polls until the change is visible at both tiers:
#!/usr/bin/env bash
# scripts/measure-propagation.sh — how long until a purge is visible at each tier
set -euo pipefail
HOST=www.example.com
EDGE_IP=203.0.113.10
SHIELD=https://shield.example.com
CANARY=/canary.txt
STAMP=$(date +%s%N)
# 1. Write a new value at the origin.
printf '%s\n' "$STAMP" | aws s3 cp - "s3://assets-origin${CANARY}" \
--content-type text/plain --cache-control "no-cache" >/dev/null
# 2. Purge both tiers, shield first.
curl -sf "${SHIELD}/purge${CANARY}" >/dev/null
curl -sf -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json" \
--data "{\"files\":[\"https://${HOST}${CANARY}\"]}" >/dev/null
START=$(date +%s%N)
poll() {
local label="$1" url="$2" resolve="${3:-}"
while true; do
if [ -n "$resolve" ]; then
body=$(curl -s --resolve "$resolve" "$url")
else
body=$(curl -s "$url")
fi
if [ "$body" = "$STAMP" ]; then
printf '%-8s visible after %d ms\n' "$label" $(( ($(date +%s%N) - START) / 1000000 ))
return 0
fi
sleep 0.1
done
}
poll shield "${SHIELD}${CANARY}"
poll edge "https://${HOST}${CANARY}" "${HOST}:443:${EDGE_IP}"
# shield visible after 214 ms
# edge visible after 1382 ms
Run it a dozen times and take the 95th percentile, not the median — the number you need is the one that decides whether a deploy gate is flaky. If the edge figure is consistently far above the shield figure plus the vendor’s published propagation time, the likely cause is purge ordering: an edge purged before the shield re-pulls the stale object and only clears on its next natural expiry. The correct sequencing is described in the multi-CDN purge orchestration guide.
When to Reconsider the Shield
Your origin is already at the edge. If HTML and assets are served from a Worker, a Function, or object storage replicated to every region, the shield’s fetch saving is small and its purge cost is not. Skip it.
You purge HTML constantly. A site deploying twenty times a day with a 30-second HTML TTL spends more time in propagation windows than a shield’s offload is worth. Either lengthen the TTL and rely on purge, or shorten it and rely on expiry — the trade is analysed in immutable Cache-Control and TTL tuning.
Your working set does not fit. A shield with a hit ratio under about 60% on assets is adding a hop for nothing. Fix the sizing or remove the tier.
You cannot tolerate a shared failure domain. In a two-provider setup, one shared shield is a component whose failure takes both providers’ misses with it — precisely the correlation a second provider was bought to avoid. Either give each provider its own shield or accept that the shield, not the CDN, is now your availability ceiling. That constraint interacts directly with a cutover, as DNS failover and orphaned fingerprinted assets sets out.
Conversely, keep the shield if hashed assets dominate your traffic and HTML is a rounding error in request volume — which is the normal shape of a fingerprinted site. Then the shield’s cost applies to a handful of purged documents per deploy and its benefit applies to everything else.
Related
- Multi-CDN and origin shield purging — the parent guide to shields and purge fan-out
- Split-brain caches across two providers — what a half-cleared tier looks like to users
- DNS failover and orphaned fingerprinted assets — the shield as a shared failure domain during a cutover
- Tiered cache and hashed asset propagation — Cloudflare’s implementation of the upper tier
- CloudFront invalidation — Origin Shield and invalidation timing on AWS