DNS and CDN
Follow name resolution and edge caching, then test cache keys, revalidation, expiry, and private responses.
DNS resolves names into records that help a client reach a service. A content delivery network (CDN) receives HTTP requests at distributed edge locations and may answer from a stored representation. These are separate decisions: reaching an edge says nothing about whether that edge has a usable copy of the requested response.
Prerequisites: understand URLs, HTTP headers, status codes, and client/server connections from Request and Response. The examples use Node.js 22 or newer, curl, and optional dig. Download the complete lab, extract it, and run commands inside its directory. It uses only Node's standard library and local loopback ports; no CDN account is needed.
DNS answers have owners and lifetimes
An application's stub resolver usually asks a recursive resolver to find an answer. On a cache miss, that resolver can follow referrals: a root server identifies the appropriate top-level-domain servers, which identify the authoritative servers for the requested zone. An authoritative server publishes the zone's records. Cached delegations and answers often shorten this process.
An A record supplies an IPv4 address, AAAA supplies IPv6, and CNAME points to another DNS name whose records must be resolved. DNS does not choose /products/42 or inspect its HTTP authorization header. Its input is a name and record type, independently of an application's URL path.
If dig and outbound DNS are available, compare a recursive answer with an authoritative answer. First discover an authoritative server; substitute one returned name in the third command:
dig example.com A +noall +answer +comments +time=2 +tries=1
dig example.com NS +short +time=2 +tries=1
dig @elliott.ns.cloudflare.com example.com A +norecurse \
+noall +answer +comments +time=2 +tries=1
# Observed: recursive flags included ra; authoritative flags included aa.
# The discovered names and returned addresses can change.ra advertises recursion availability; aa identifies an authoritative answer. Neither is a latency measurement. The number before IN A is a TTL in seconds. A resolver ordinarily reuses an answer within its remaining TTL. Repeating queries need not show a steadily decreasing value: resolver pools, refreshes, and different upstream servers can change the observation.
Changing authoritative data cannot recall existing cached answers. Before a planned endpoint migration, lower the TTL early enough for the previous lifetime to expire, then change the address while keeping the previous endpoint available during the transition. Lower TTLs increase lookup traffic; longer TTLs reduce it but extend ordinary cache lifetimes. Negative answers can also be cached, so creating a previously absent name may not become visible immediately. “DNS propagation” combines several independent caches and publication steps; one countdown does not describe every client. Resolvers may also implement serve-stale behavior during upstream failures.
Reaching an edge is different from hitting its cache
A common request path is:
DNS lookup → edge address
client ── TLS ──> CDN edge ── separate TLS ──> origin
│
└── reusable cached representationSome providers advertise the same address from several locations using anycast. Network routing determines the path; the selected location need not be the geographically closest one. Other deployments use DNS steering to return different endpoints. Either approach can land on an edge with an empty cache.
The origin produces the response; the edge can reuse eligible copies to reduce origin traffic and avoid an origin round trip. A CDN can also proxy dynamic requests without caching them. Large static objects and public catalog responses are useful candidates, while a personalized account response needs a deliberate privacy policy. A cache hit still requires sending bytes to the client, and multiple locations can each miss for the same object.
Start the lab in one terminal, then issue the two requests promptly in another:
node dns-cdn-lab.mjs
# Origin: 127.0.0.1:9100; teaching edge: 127.0.0.1:9101.curl -si http://127.0.0.1:9101/asset
curl -si http://127.0.0.1:9101/asset
# X-Lab-Cache: MISS, then HIT; identical X-Origin-Request values.
# A hit can legitimately have Age: 0 when reused immediately.curl does not cache these requests. The origin supplies Cache-Control: public, max-age=0, s-maxage=2 and ETag: "v1". A browser gets no fresh reuse period from max-age=0; a shared cache uses the two-second s-maxage instead. Age represents estimated response age, rather than how recently this client requested it. Production calculations include upstream age and transit time; the lab has one origin and simplifies this to elapsed time since storage.
The fixture is a teaching cache for fixed text endpoints, not production CDN software. It has no TLS, eviction, compression variants, distributed locations, or background refresh. Stop it with Ctrl-C. The complete tests use isolated ephemeral ports instead of requiring 9100 and 9101 to be free.
A cache key is a claim that responses are interchangeable
A cache associates an HTTP method and target URI with stored responses. If /asset?size=small and /asset?size=large produce different bytes, removing size from the key would make an incorrect equivalence claim. Preserving every query parameter is safe for this lab, though tracking parameters can fragment a production cache. Only omit or normalize inputs after establishing that they cannot change the representation.
Vary adds representation-selection inputs. For /greeting, the origin declares Vary: Accept-Language. The lab's fixed-origin, GET-only key includes the full target plus the relevant language value:
const target = url.pathname + url.search;
const language = url.pathname === '/greeting'
? req.headers['accept-language'] || '' : '';
const key = JSON.stringify([target, language]);
// Different language values cannot overwrite one another's entry.Test repeated variants and two identities:
curl -si -H 'Accept-Language: en' http://127.0.0.1:9101/greeting
curl -si -H 'Accept-Language: fr' http://127.0.0.1:9101/greeting
curl -si -H 'Accept-Language: en' http://127.0.0.1:9101/greeting
curl -si -H 'Accept-Language: fr' http://127.0.0.1:9101/greeting
# Hello and Bonjour remain separate; immediate repeats are HITs.
curl -si -H 'Authorization: Bearer alice' http://127.0.0.1:9101/me
curl -si -H 'Authorization: Bearer bob' http://127.0.0.1:9101/me
# alice and bob; both BYPASS with Cache-Control: private, no-store.private prohibits storage by shared caches; no-store prohibits storage by caches generally. no-cache permits storage but requires successful validation before reuse. These directives control reuse, not access authorization or transport encryption.
Requests containing Authorization have shared-cache restrictions, but explicit directives such as public or s-maxage can permit reuse. A Cookie request or Set-Cookie response is not, by itself, a universal prohibition on storage. Here the edge conservatively bypasses every authenticated or cookie-bearing request and every response setting a cookie; the origin also marks session-dependent responses private and non-storable. This trades hit rate for a simple, inspectable isolation rule. It does not rely on putting a secret token into a shared key.
The lab rejects supplied forwarding-host headers and forwards only the origin inputs it understands. Otherwise, an attacker-controlled header might change generated links without changing the cache key, leaving a poisoned representation for later users. Key design must follow actual application behavior, including tenant selection and content negotiation.
Expiration asks a question; purge removes a copy
Becoming stale does not mean an object changed. An ETag lets the edge ask whether its stored representation still matches. Inspect the origin directly:
curl -si -H 'If-None-Match: "v1"' http://127.0.0.1:9100/asset
# 304 Not Modified with no response body.After expiry, the edge sends its stored ETag upstream. A matching validator yields 304: refresh applicable metadata and reuse the stored body. A changed validator yields 200 with replacement bytes. Validation saves body transfer when unchanged, but still contacts the origin; a fresh hit avoids that request.
The verifier advances an injected clock and changes origin state. This excerpt shows why both outcomes matter:
seconds = 2;
let c = await get('/asset');
check(c.headers.get('x-lab-cache') === 'REVALIDATED'
&& c.body === a.body, 'exact expiry validates unchanged ETag');
lab.state.version = 2;
seconds = 4;
c = await get('/asset');
check(c.headers.get('x-lab-cache') === 'REFRESH'
&& c.body.includes('v2'), 'changed validator replaces body');
// Run the full script, which defines get/check and starts both servers:
// node dns-cdn-lab.mjs verifySeparate /stale responses use public, max-age=2, stale-if-error=3: the lab permits an old copy after an origin 503 for a bounded additional interval, then returns the error. /strict adds must-revalidate and rejects stale reuse. The /asset policy's s-maxage also incorporates shared-cache revalidation requirements, so it is deliberately separate. stale-while-revalidate instead allows bounded stale delivery during background validation; the lab does not implement it. Provider support and configured precedence must be checked before relying on these extensions.
The verifier also purges an entry through lab.purge('/asset'), then asserts a MISS. A CDN purge targets its own entries, potentially across locations and variants; it cannot remove already cached browser copies. Content-hashed asset URLs make new deployments refer to new objects without changing the meaning of old URLs. Keep old assets available while older pages still reference them.
Finally, validate origin TLS independently of client TLS and restrict origin access where edge controls must not be bypassed. Load balancing chooses destinations and gateways apply API routing and policy; the next lesson develops those mechanisms. Application cache eviction and stampedes belong to Caching. Here the essential decision is whether this particular HTTP representation is safe and fresh enough to reuse.