CDN Architecture: Edge Caching, Anycast, Origin Shield and Cache Invalidation Explained (2026)

CDN Architecture: Edge Caching, Anycast, Origin Shield and Cache Invalidation Explained (2026)

CDN Architecture: Edge Caching, Anycast, Origin Shield and Cache Invalidation Explained (2026)

A content delivery network looks like a simple trick: copy files near users. In production it is a layered distributed system with its own routing plane, its own consistency model and its own failure modes, and most outages blamed on “the CDN” are really mistakes in how an origin talks to that system. A cache key that is slightly too wide turns a 95% hit ratio into 40%. A purge that is slightly too narrow leaves stale firmware on a million devices. A missing shield tier lets one popular object stampede an origin that was sized for the average case.

This guide explains CDN architecture from the request path outward: how a client lands on a point of presence (PoP), how the edge decides whether a response is reusable under RFC 9111, how tiered caches and an origin shield collapse duplicate fetches, and how invalidation really propagates. It uses hit-ratio arithmetic you can redo on your own traffic, plus a small simulation whose results I ran for this article. The IoT angle runs throughout, because a fleet downloading firmware is the hardest cache workload most teams will ever see.

What this covers: the request path and routing, cache keys and freshness rules, shielding and request coalescing, invalidation design, HTTP/3 and TLS at the edge, edge compute, firmware delivery for device fleets, failure modes, and a decision checklist.

Context and Background

Commercial CDNs predate the cloud. Akamai, founded in 1998, built its model around caches deployed inside ISP networks, and the problem it solved was the same one we have today: the speed of light and congested peering links dominate latency for distant users. What changed is the shape of traffic. Video, software updates, API responses and machine-to-cloud telemetry now share the same edge, and the edge itself runs code.

Three architectural families exist in practice. The first is the classic DNS-steered CDN, where an authoritative name server answers each lookup with the address of a nearby cache chosen from latency and load measurements. The second is the anycast CDN, where many PoPs announce the same IP prefixes over BGP and the internet’s routing system delivers each client to a nearby site. The third is the hybrid that most large providers actually run: anycast for the front door, DNS or internal mapping for specific services. If you want the DNS side in depth, our post on DNS anycast, GeoDNS and global server load balancing covers resolver behavior, TTLs and failover.

The caching rules are not vendor inventions. HTTP caching semantics are defined in RFC 9111, an Internet Standard (STD 98) published in June 2022 that obsoleted RFC 7234. Two extensions matter at the edge: RFC 5861 (May 2010, informational) defines stale-while-revalidate and stale-if-error, and RFC 9213 (June 2022, proposed standard) defines CDN-Cache-Control, a header targeted at CDN caches so that origins can give a CDN different instructions than browsers receive. Vendors layer proprietary controls on top, such as Surrogate-Control and surrogate keys, but the standard vocabulary carries most of the weight.

One framing mistake is worth removing early. A CDN is not a mirror of your site. It is a set of independent, bounded, eventually consistent caches that each make local decisions from headers, and your job as the origin owner is to make those decisions predictable. The rest of this post follows that framing: what each layer decides, what information it has, and what happens when it decides wrong.

External reading for the specifications: the RFC 9111 text is short enough to read in one sitting and worth doing once.

The Reference Architecture: From Client to Origin

A CDN request passes through five decisions: which PoP receives the packets (anycast or DNS), whether the edge cache holds a fresh copy, whether an upper cache tier or shield holds one, whether another request is already fetching it (coalescing), and only then the origin. Each layer exists to absorb misses from the layer below it.

CDN architecture request path from client through anycast PoP, edge cache, shield tier and origin

Figure 1: The layered request path. Each tier absorbs misses so the origin sees a small fraction of client traffic.

The figure shows the lookup order. A client resolves a hostname, the packets reach a PoP, and a cache node inside that PoP checks its local store. On a miss the node asks an upper tier, which might be a regional hub, a designated shield PoP, or both. Only a miss at every tier reaches your origin. Along the way, a coalescing mechanism makes concurrent requests for the same key wait on one fetch rather than each opening their own.

Getting to a PoP: anycast and its limits

In anycast routing, the same prefix is announced from many locations, and BGP route selection (shortest AS path, local preference and so on) decides which announcement each network prefers. BCP 126, published as RFC 4786, documents the operational practice. The appeal is that failover is automatic: withdraw the announcement from a failing site and traffic shifts to the next-best one within BGP convergence time, which is typically seconds to a few minutes depending on the path.

The weakness is that BGP does not measure latency. It prefers fewer AS hops and operator policy, so a client can be routed to a PoP that is topologically close but geographically far, or to one that is overloaded. Providers address this with traffic engineering, selective announcements, and by splitting prefixes so that different services or tiers sit on different anycast groups. The second weakness concerns long-lived connections. If routes change mid-connection, packets can land on a different PoP that holds no TCP state, and the connection resets. QUIC mitigates this because a connection is identified by connection IDs rather than the 4-tuple, which lets a deployment route packets to the right backend even when the path changes (RFC 9000 defines the connection ID mechanism, and the QUIC-LB work in the IETF addresses stateless load-balancer routing). I treat the exact deployment details of each vendor as undisclosed unless they publish them.

DNS-based steering has the opposite profile. It can make informed choices using real measurements, but it only sees the resolver’s address rather than the client’s (unless EDNS Client Subnet is used), and its decisions are cached for the record TTL. In practice the choice is not either/or. Many providers use anycast for their DNS servers and for the edge, and let internal mapping systems choose among services behind that front door.

Inside the PoP: node selection and local caches

A PoP is rarely one server. It is a rack-scale cluster behind an internal load balancer (an ECMP and consistent-hashing layer in many designs). Consistent hashing of the cache key sends each object to a small subset of nodes, which keeps the working set from being duplicated on every machine and raises the effective cache size of the site. Fastly’s documentation notes that objects may be cached on multiple machines within a POP, and that very popular objects can end up on every node, which is why X-Cache-Hits reflects a single node’s count. Hot-object replication is a deliberate trade: it spends storage to avoid making one node a bottleneck.

For the mechanics of balancing across nodes, including Maglev-style consistent hashing and power-of-two-choices, see our guide to load balancing algorithms. The relevant property here is that the cache key, not the client address, should drive node selection for cacheable traffic.

The upper tiers: regional hubs and shields

Cloudflare’s documentation describes Tiered Cache as splitting data centers into a lower tier close to visitors and an upper tier that is the only layer allowed to contact the origin. Its topologies differ in how the upper tier is chosen: Smart Tiered Cache picks the single closest upper tier per origin using latency data, Generic Global Tiered Cache groups data centers into regional upper-tier pools and hashes the cache key to select one, and Regional Tiered Cache inserts a regional hub between the two. Per the documentation, Smart topology is available on all plans while Generic Global, Regional and Custom topologies are Enterprise features.

Amazon CloudFront describes Origin Shield as an optional caching layer, configured per origin, that all edge locations and regional edge caches pass through before the origin, so that requests for an object absent from the shield are consolidated and the origin may receive as few as one request for it. Fastly’s shielding works similarly: a designated shield POP is configured per backend, and misses from all other POPs funnel through it. The common idea is a single choke point whose cache is warm for the whole network. The common cost is an extra hop on a miss, and additional request charges on some platforms.

Cache Keys, Freshness and Revalidation

The cache key decides whether two requests are “the same”, and the freshness rules decide how long a stored response may be reused. Together they determine your hit ratio more than any hardware does. RFC 9111 specifies the minimum key as the request method plus the target URI, extended by whatever the Vary header nominates.

The cache key and key explosion

A key that includes too little serves the wrong content to some users. A key that includes too much fragments the cache into near-duplicates that never get hit. The second failure, cache key explosion, is quieter and more common.

Consider an image endpoint /img/hero.jpg that accepts w, h, q, fmt and a tracking parameter utm. If w has 6 values, h has 6, q has 3, fmt has 2 and utm has 1,000 distinct campaign values, the distinct URLs number 6 × 6 × 3 × 2 × 1,000 = 216,000 for one image, although only 216 variants differ in what the user sees. If the cache key includes the query string verbatim, 99.9% of the key space is waste. Worse, parameter order creates more variants: ?w=1&h=2 and ?h=2&w=1 are different strings unless the CDN normalizes them. With five parameters, there are 5! = 120 possible orderings of the same content.

The fixes are mechanical. Strip parameters that do not affect the response (tracking and session parameters) from the key. Sort the remaining parameters. Allow-list the parameters the origin actually uses. Normalize the Accept-Encoding header into a few buckets (identity, gzip, br) before it enters the key, instead of caching per raw header value, because browsers send many spellings of nearly the same preference.

Vary, and why it is dangerous at the edge

Vary: Accept-Encoding is benign if the edge normalizes the header. Vary: User-Agent is nearly always a mistake, because User-Agent strings number in the tens of thousands and each becomes its own cache entry. Vary: Cookie effectively disables shared caching for any page where cookies differ by user. RFC 9111 also states that a Vary value of * never matches, so such responses cannot be reused without revalidation. If the response truly differs per user, mark it private or no-store and stop pretending it is cacheable.

Freshness lifetime: what the cache computes

RFC 9111 section 4.2.1 gives the order of precedence for a stored response. A shared cache uses s-maxage if present, else max-age, else Expires minus Date, else a heuristic. The heuristic applies only when no explicit expiration exists and the response is heuristically cacheable. The RFC says caches are encouraged to use a fraction of the time since Last-Modified, and that “a typical setting of this fraction might be 10%”. So a file last modified 100 days ago, served with no Cache-Control, may be considered fresh for 10 days. That is a trap for anyone who relies on defaults: the origin said nothing, and the cache decided something large.

The Age header lets each tier report how long a response has been in caches, so that downstream caches subtract it from the lifetime rather than restarting the clock. This matters with tiering: an object that sat for 50 seconds in a shield is delivered to an edge with Age: 50, and if max-age=60, the edge treats it as fresh for only 10 more seconds. Teams that see “mysteriously short” edge lifetimes usually have this tiered Age accounting at work.

Directives that actually matter

no-store forbids storing any part of the request or response. no-cache allows storing but requires successful validation before reuse, which is a common confusion: no-cache is not “do not cache”. private bars shared caches from storing the response. public explicitly permits storage even for requests carrying Authorization. must-revalidate forbids serving stale after expiry without validation, and per the RFC the cache should return an error, typically 504, when it cannot reach the origin to validate. s-maxage targets shared caches and overrides max-age. A reasonable default for a versioned static asset is Cache-Control: public, max-age=31536000, immutable, with the version embedded in the filename so a change produces a new URL and no purge is needed.

stale-while-revalidate and stale-if-error

RFC 5861 defines two extensions that convert latency and availability risk into controlled staleness. With stale-while-revalidate=N, a cache may serve a stale response for up to N seconds after expiry while attempting revalidation in the background. With stale-if-error=N, a cache may serve stale content when the origin would produce a 500, 502, 503 or 504, for up to N seconds of staleness. Note that the RFC is informational, not standards track, yet CDNs implement it widely. Whether a specific CDN honors the directives at the edge, at the shield, or both is a per-vendor matter you should test rather than assume.

A practical header for an API response that changes every minute but can tolerate brief staleness is:

Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=86400
CDN-Cache-Control: max-age=120
Surrogate-Key: catalog product-4711

Here browsers use the 60-second lifetime, while CDN caches that support RFC 9213 use CDN-Cache-Control instead and keep the object for 120 seconds. RFC 9213 notes a caveat: a CDN may hold a response fresh longer than other caches, so design for the premature-staleness case in downstream caches. Surrogate-Key is a Fastly convention for tagging responses so they can be purged as a group; other vendors use their own tag headers (Cloudflare calls them cache tags), so treat the header name as vendor-specific.

Cache decision flow showing freshness check, revalidation with conditional request, and stale serving paths

Figure 2: The edge decision flow. A fresh hit returns immediately, a stale entry within the stale window is served while a background revalidation runs, and a hard miss goes to the upper tier.

The flow in Figure 2 is the logical core of every edge cache regardless of vendor. The branches that save you during incidents are the two stale paths: they turn an origin outage or slow origin into degraded freshness rather than errors.

Shielding, Coalescing and Hit-Ratio Arithmetic

The previous sections described what each cache does alone. The interesting engineering is in how they combine, because a multi-tier system has a hit ratio at each layer and the origin only sees what survives all of them.

Request coalescing and the thundering herd

When a popular object expires or is purged, every request that arrives before the first refetch completes is also a miss. Without protection, each of those misses opens its own origin connection. This is the thundering herd (also called a cache stampede or dogpile). Little’s Law gives the size of the problem: the number of concurrent origin fetches equals the arrival rate times the fetch duration. At 2,000 requests per second for one object and a 400 ms origin fetch, L = 2,000 × 0.4 = 800 simultaneous requests, all for the same bytes.

Request coalescing (also called collapsed forwarding or a cache lock) fixes this. The first miss for a key becomes the leader and fetches; the others wait on the in-flight fetch and receive the same response. Origin concurrency for that key drops from 800 to 1. The trade-off is head-of-line blocking: if the leader’s fetch is slow, every follower waits for it, and if the response turns out to be uncacheable (for example, Set-Cookie or private), followers must be released to fetch individually, serialising them at the worst moment. Good implementations detect uncacheable responses and stop coalescing that key. I could not verify from vendor documentation how each major CDN implements the lock, so the behaviour described here is the general design, and the specific timeout and release rules are vendor details you should look up for your platform.

Coalescing works per node, per tier. A PoP with 40 nodes can still send 40 simultaneous fetches upward if each node coalesces independently, which is part of why consistent-hash routing of keys to nodes inside a PoP matters. At the shield tier, the same logic repeats across PoPs, which is what makes a shield valuable: even if every PoP coalesces locally, hundreds of PoPs may each send one request, and the shield merges those into one.

Hit ratio across tiers

Define the edge hit ratio h_e as the fraction of client requests served from the edge cache, and the shield hit ratio h_s as the fraction of edge misses served from the shield. The origin sees the fraction (1 − h_e)(1 − h_s) of client requests.

Take illustrative numbers: 1,000 requests per second at the edge, h_e = 0.90 and h_s = 0.70. The edge passes 100 requests per second upward, the shield absorbs 70 of them and the origin receives 30. Offload is 97%. Without the shield, the origin would receive 100 requests per second, more than three times as much. The shield’s benefit grows when traffic is spread across many PoPs, because each PoP sees only a slice of the popularity distribution, so its h_e is lower for the long tail while the shield, seeing the aggregate, has a much higher hit ratio on those same objects.

Two measurement traps follow. First, hit ratio can be counted by requests or by bytes, and the two diverge when large objects are cold and small ones are hot; for origin egress cost, byte hit ratio is the number that matters. Second, tiering changes how logs count. Fastly’s documentation notes that an edge miss that becomes a shield hit is recorded as one hit and one miss, and a request that reaches the origin is counted as two misses, so naive aggregate hit ratio from raw logs understates offload. Compare per-tier ratios, not one blended figure.

A simulation you can rerun

To make the tiering effect concrete, I wrote a small LRU simulation: 20 PoPs, each with an LRU cache of 2,000 objects, one shield with an LRU cache of 20,000 objects, a catalog of 100,000 objects, Zipf-distributed popularity with exponent 0.9, and 600,000 requests assigned to PoPs at random. The sizes are arbitrary. This is a toy model, not a benchmark of any product, and real traffic is burstier and has varying object sizes and TTLs.

import random, itertools, bisect
from collections import OrderedDict

class LRU:
    def __init__(self, cap): self.cap, self.d = cap, OrderedDict()
    def get(self, k):
        if k in self.d:
            self.d.move_to_end(k); return True
        return False
    def put(self, k):
        self.d[k] = 1; self.d.move_to_end(k)
        if len(self.d) > self.cap: self.d.popitem(last=False)

def zipf_sampler(n, s, rng):
    w = [1/(i**s) for i in range(1, n+1)]
    cum = list(itertools.accumulate(w)); tot = cum[-1]
    return lambda: bisect.bisect_left(cum, rng.random()*tot)

def run(pops=20, catalog=100_000, s=0.9, edge_cap=2_000,
        shield_cap=20_000, reqs=600_000, seed=7):
    rng = random.Random(seed); draw = zipf_sampler(catalog, s, rng)
    edges = [LRU(edge_cap) for _ in range(pops)]; shield = LRU(shield_cap)
    eh = sh = org = 0
    for _ in range(reqs):
        k = draw(); e = edges[rng.randrange(pops)]
        if e.get(k): eh += 1; continue
        if shield.get(k): sh += 1
        else: org += 1; shield.put(k)
        e.put(k)
    return eh/reqs, sh/reqs, org/reqs

With seed 7, the run I executed produced an edge hit ratio of 0.403, a shield hit share of 0.294 of all requests, and an origin share of 0.302 with the shield. Rerunning the same traffic with the shield removed (each edge miss goes directly to the origin) gave an origin share of 0.597. In this toy workload the shield halves origin traffic, and the reason is visible in the structure: the long tail is too large for any single PoP to hold, but the shield sees the union of all 20 PoPs’ misses and keeps the objects they collectively want. Your numbers will differ; the useful takeaway is the method. Replay a day of your own access logs through it before paying for a shield tier.

Hit ratio tiers showing client requests, edge misses, shield misses and origin load with example percentages

Figure 3: Request attrition across tiers. The origin receives the product of the per-tier miss ratios, so a modest shield hit ratio produces a large cut in origin load.

Choosing the shield location

Because every miss funnels through the shield, its placement adds latency to misses and should be near the origin. CloudFront’s documentation advises enabling Origin Shield in the same AWS Region as the origin when that Region is supported, and, for origins outside AWS, choosing the Region with the lowest latency to the origin. Fastly’s documentation rates shield POPs by capacity (Small, Medium or Large) and notes that cost implications vary by origin provider. Origin Shield is not free of downsides: the CloudFront documentation says it may not help with dynamic content proxied to the origin, low-cacheability content, or rarely requested content, and it bills incremental request charges in some cases.

Cache Invalidation: Propagation, Tags and Soft Purge

Phil Karlton’s old line that cache invalidation is one of the two hard problems is repeated so often it hides a useful point: the difficulty is not deleting an entry, it is deleting it everywhere, quickly, without a cost spike on the origin.

Four strategies, ranked by safety

The safest strategy is to never invalidate. Put a content hash or version in the URL (app.3f9a1c.js), serve it with a one-year immutable lifetime, and let the HTML that references it carry a short TTL. A deploy then changes the reference, not the cached object, and there is nothing to propagate.

The second is a short TTL combined with stale-while-revalidate: the content self-heals within the TTL and users never wait. The third is purge by tag or key: the origin labels responses with tags, and an event such as “product 4711 updated” purges everything tagged product-4711. The fourth and most dangerous is purge by URL or wildcard, which requires the publisher to know every URL a change touches, and which breaks the day someone adds a new listing page that embeds the product.

How fast does a purge propagate?

Vendors publish figures, and they should be read as vendor-reported. Cloudflare’s September 2024 post “Instant Purge” reports a global P50 under 150 ms for purge by tag, hostname and prefix (149 ms for August 2024, with single-file purges at a P50 of 234 ms), and describes a coreless design in which purge requests can enter any data center and keys are distributed peer to peer. Fastly’s engineering blog says copies of an object begin purging within about 5 ms, and that every POP is typically complete in about 150 ms and almost always within 250 ms, while a “purge all” is a different mechanism that can take up to a minute. That purge-all figure matters operationally: it increments a version number rather than deleting objects, so it is not an emergency stop for a bad deploy in the way people hope.

Tail latency matters more than the median. If one PoP in 300 is slow to receive a purge, 0.3% of users see stale content for longer, and your correctness argument must hold for them. Design for this by making stale content harmless, for example through versioned URLs, and treat the purge as an optimization rather than a safety mechanism.

Cost and rate limits

Invalidation can cost money. CloudFront’s documentation states that the first 1,000 invalidation paths submitted per month are free across the account, paths above that are charged, a wildcard path counts as one path regardless of how many objects it invalidates, and tag invalidations count against the same allowance as a single path each. The practical consequence is that a pipeline invalidating 2,000 specific URLs per deploy, ten deploys a day, is paying for 20,000 paths a day when a single /* or a tag would do it for ten.

Soft purge and the origin cliff

A hard purge deletes the object, so the next request is a miss, and a popular object turns into a herd that coalescing must absorb. A soft purge marks the object stale instead. Fastly’s blog explains that a soft purge requires stale-while-revalidate and stale-if-error in the response headers and recommends it for popular content and for cases where the origin may be down. The next request is served stale while one background fetch refreshes the entry. This converts an invalidation from a latency event into a quiet refresh, which is why soft purge is the right default for high-traffic objects whose staleness for a second or two is acceptable.

Cache invalidation propagation from origin event through purge API to edge PoPs and shield with soft purge path

Figure 4: Invalidation propagation. A purge call enters one PoP, fans out to all PoPs and the shield, and either deletes entries (hard) or marks them stale for background revalidation (soft).

HTTP/3, TLS and Compute at the Edge

Terminating transport close to the user

The strongest latency argument for a CDN has nothing to do with caching. A TCP plus TLS 1.3 connection to a far origin needs at least one round trip for the TCP handshake and one for TLS before the first request byte, so at a 150 ms round trip that is roughly 300 ms of setup before any data. Terminate at a PoP 15 ms away and the same setup costs about 30 ms, with the edge holding warm, reused connections to the origin or shield. Cached or not, the user benefits. For the TLS side, including session resumption and 0-RTT replay risks, see our post on the TLS 1.3 handshake, 0-RTT and mTLS for constrained IoT devices.

HTTP/3 (RFC 9114) runs over QUIC (RFC 9000) and removes TCP-level head-of-line blocking, folds the transport and TLS 1.3 handshakes into one, and supports connection migration. The edge is the natural place to deploy it because clients speak to one endpoint and the edge can speak HTTP/2 or HTTP/1.1 to the origin. Two cautions apply. UDP is blocked or throttled on some enterprise networks, so clients must fall back to TCP; and QUIC’s user-space implementation costs more CPU per byte than kernel TCP in many deployments, which can matter at the edge. I did not find a neutral, current benchmark to quote, so I make no throughput claims here.

Security properties of the edge TLS

Terminating TLS at the edge means the CDN holds your certificate private key or issues certificates on your behalf, and it sees plaintext. That is a trust decision, not a technical detail. Mitigations include keyless TLS arrangements where signing is delegated to your infrastructure, re-encrypting to the origin with verified certificates (never “flexible” modes that leave the origin leg unencrypted), and authenticated origin pulls so the origin accepts connections only from the CDN. Origin lockdown matters: if your origin IP is discoverable and reachable from the open internet, attackers bypass the CDN and its protections.

Edge compute

Many CDNs now run user code at the PoP, in isolates or WebAssembly sandboxes rather than containers, for tasks like request rewriting, A/B routing, authentication checks and API aggregation. The architectural question is where that code runs relative to the cache. Fastly’s shielding documentation notes that edge code usually runs at the visitor’s POP and again at the shield, and only once if the shield is the closest POP, so code that is not idempotent needs care, and client-facing response changes should happen at the client-connected POP. Compute at the edge is best for decisions that depend only on the request, and worst for work that needs strongly consistent state from a distant database, because each call pays the latency you were trying to avoid.

Delivering Firmware to IoT Fleets Through a CDN

Over-the-air (OTA) updates are the workload CDNs were not designed for and are nevertheless good at. Firmware images are large, immutable and requested by a huge synchronized population, which is nearly the ideal cacheable object, with one serious problem: the arrival pattern.

Sizing the surge

Suppose a fleet of 1,000,000 devices must fetch a 20 MB image. The total transfer is 20 TB. If devices are told to update at a fixed time and all do so within 10 minutes, the average demand is 1,000,000 / 600 ≈ 1,667 downloads per second, or about 33 GB/s, which is roughly 267 Gbps. If the same fleet spreads over 24 hours with random jitter, demand falls to about 11.6 downloads per second, about 231 MB/s or 1.85 Gbps. Same bytes, a 144-fold difference in peak. A CDN can absorb the first case, but your own origin cannot, and constrained devices on cellular links will time out and retry, multiplying load. Jittered check-in times and staged rollouts (1%, 10%, 50%, 100%) are the cheapest capacity control you have.

With a 97% offload (illustrative), the origin serves 3% of 20 TB, about 600 GB, instead of 20 TB, and the shield means the origin sees roughly one fetch per PoP-region per image rather than one per PoP. These figures are arithmetic, not measurements of any real fleet.

Cache-friendly OTA design

Address the image by content hash or version in the URL, so it can be cached for a year and never purged. Keep the small, mutable manifest, which says “device model X should run version Y”, on a short TTL with stale-while-revalidate. Devices fetch the manifest through the API and the image through the CDN, and a rollback is a manifest change, not a purge.

Support HTTP range requests so that interrupted downloads resume. Check how your CDN caches range requests: some fetch and cache the whole object on the first range request, others cache by range, and the difference changes both hit ratio and origin load. Test with real devices, not curl. Sign the image and verify the signature on the device (for example with an Ed25519 or ECDSA signature checked against a key in the bootloader), so that a compromised CDN account or cache cannot deliver a malicious image. Integrity must not depend on the transport path.

Use short-lived signed URLs or tokens if images are not public, and make the token validation happen at the edge. Be careful that token query parameters are excluded from the cache key (or the CDN’s token feature handles this natively), otherwise every device has a unique URL and the hit ratio is zero. Finally, constrained devices may only support a limited set of TLS cipher suites and certificate chains; test that the CDN’s certificate chain and TLS version set work with your devices’ trust stores, since a CDN-managed certificate rotation can strand old firmware.

Trade-offs, Gotchas, and What Goes Wrong

Cache poisoning and unkeyed inputs. If the origin builds a response from a request header that the cache key ignores (for example X-Forwarded-Host), an attacker can send one crafted request, get a poisoned response stored, and have it served to everyone. James Kettle’s 2018 research on web cache poisoning popularised the pattern. The defence is to make the key and the response agree: either include every input that changes the output in the key, or make the origin ignore the input. Strip unexpected headers at the edge.

Caching personalised responses. A missing Cache-Control: private on a response containing user data, combined with a CDN rule that caches by path, is the classic way to leak one user’s page to another. Cache deception attacks use a path such as /account/profile/x.css to trick a cache into storing a dynamic page because of its extension. Default to not caching anything without an explicit allow rule, and never let file extension alone decide cacheability.

Error caching. A 5xx or an empty 200 cached for the full TTL turns a one-minute origin blip into an hour-long outage. Set short, separate TTLs for error responses and enable stale-if-error so that good content outlives bad fetches.

Origin bypass and capacity cliffs. The origin is sized for the miss stream, which is calm until a global purge, a new release or a cache restart makes everything cold. Test the cold-cache case: multiply the steady-state miss rate by the hit-ratio drop and compare against origin capacity. Shielding and coalescing reduce the peak, but do not remove it.

Header trust. The client IP seen by your origin is the shield’s or CDN’s. Fastly’s documentation notes the true client IP arrives in Fastly-Client-IP rather than the connection address, and other CDNs use their own headers. Only trust such headers from the CDN’s published address ranges, or an attacker can spoof them. RFC 9209’s Proxy-Status header exists to expose per-hop diagnostics and is worth logging.

Observability gaps. Edge logs are your only view of most traffic, and they are sampled or delayed on some plans. Capture X-Cache-style headers, per-tier hit status and origin fetch time. Without them, a hit-ratio regression looks like an application bug.

Multi-CDN

Running two CDNs reduces provider risk and lets you steer by performance or cost, typically via DNS-level traffic steering. The price is real: two cache keys to keep identical, two invalidation APIs to fan out to (and a purge that succeeds on one and fails on the other), two sets of logs to normalise, a feature set limited to the common denominator, and doubled origin warming because each CDN fills its own cache. Multi-CDN pays off for large video or software distribution and for organisations whose availability target exceeds what one provider’s contract offers; for most teams, one CDN plus a tested origin-direct fallback is the better trade.

Practical Recommendations

Treat the CDN as part of your application, with versioned configuration, tests and an owner. Start from content: classify every route as immutable (hashed assets and firmware), short-lived shared (catalog, public API), or private, and write the headers for each class once in a shared library instead of per service.

Then tune the key. Remove tracking parameters, sort the rest, bucket Accept-Encoding, and review every Vary. Add a shield close to the origin when your PoP count is high and your long tail is large, and measure the per-tier hit ratios before and after. Replay a day of access logs through a simulator like the one above to estimate the benefit before committing to a paid tier.

For invalidation, prefer versioned URLs, then tag-based soft purge, and keep full purges for emergencies that you have rehearsed. For fleets, jitter the check-in time and stage rollouts.

  • Classify routes: immutable, shared short-lived, private.
  • Use public, max-age=31536000, immutable with hashed filenames for static assets and firmware.
  • Add stale-while-revalidate and stale-if-error to every shared dynamic response.
  • Normalise the cache key and audit Vary.
  • Enable a shield or tiered cache and track per-tier and byte hit ratio.
  • Lock the origin to the CDN and verify certificates on the origin leg.
  • Tag responses for purge and rehearse a cold-cache event.
  • Sign firmware on the device side and jitter fleet downloads.

Frequently Asked Questions

What is the difference between a CDN and a reverse proxy cache?

A reverse proxy cache such as Varnish or NGINX is software you operate in a few locations. A CDN is that same idea operated as a global service: many PoPs, a routing layer (anycast or DNS) that sends users to a nearby one, shared tiers, purge propagation, DDoS absorption and TLS termination. You can build a small CDN yourself, but the routing and operations are the expensive parts.

Does a CDN cache dynamic content?

It can cache anything whose response is safe to share, including API results, if headers permit. Truly personalised responses should be private or no-store. Even for dynamic content that cannot be cached, a CDN helps by terminating TLS near the user and reusing warm connections to the origin, which cuts connection setup latency.

What is an origin shield and do I need one?

An origin shield is a designated cache tier that all other PoPs query before the origin, so duplicate misses merge into one. You likely need it if you have many PoPs, a large long tail of objects or an origin that is expensive or fragile. Its value is small for tiny catalogs or highly dynamic traffic.

How long does cache invalidation take?

Vendor-reported figures for modern platforms are on the order of 150 ms at the median, with Fastly reporting almost all POPs within 250 ms. Treat that as a median, not a guarantee, and design so that brief staleness is harmless. A global purge-all can take up to a minute on some platforms.

What does stale-while-revalidate do?

Defined in RFC 5861, it lets a cache serve an expired response for a set number of seconds while it fetches a fresh copy in the background. Users get a fast answer and the origin sees a single refresh instead of a herd. It pairs with stale-if-error, which serves stale content during origin 5xx errors.

Is anycast better than DNS-based routing?

Neither dominates. Anycast gives fast failover and no resolver dependence, but BGP does not measure latency and route changes can disrupt long TCP connections. DNS steering can use measurements and load, but is subject to TTLs and resolver location. Most large CDNs combine them.

Further Reading

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *