CloudFront.
AWS's content delivery network — ~600 edge POPs that cache responses from your origins and execute small bits of code at the edge. Most pages on AWS sit behind CloudFront; static sites front S3 with it, dynamic apps front their ALB with it. The interesting parts are behaviours, the two flavours of edge compute, and how Origin Access Control replaces the old origin-identity dance for private S3 buckets.
1 · What a CDN actually is (and isn't)
A content delivery network is, at root, a geographically distributed reverse-proxy cache. The mental model that survives every conversation: clients hit a server in their own metro instead of crossing oceans to your origin, that server reuses one cached response for everyone who asks for the same thing, and your origin only sees the small fraction of requests the cache couldn't serve. Everything else CloudFront does — TLS termination, edge compute, signed URLs, header rewriting — hangs off that single primitive.
What a CDN isn't: a magic speed-up button. If every request has a different cache key (auth header in the key, unique query string, no caching headers from origin), CloudFront becomes an expensive HTTPS pass-through with extra latency on top. The hit-rate number is the only metric that matters when you're judging whether the CDN is doing its job.
Two architectural choices to internalise. Pull vs push: CloudFront is pull-based — content lands at an edge only when a viewer requests it and the cache misses. There's no upload-to-CDN step; you upload to S3 (or your origin), and the edge populates on demand. Push CDNs like Cloudflare R2's replication or fastly's pre-warm exist, but they're the exception. Request coalescing: when a thousand viewers ask for the same uncached object at the same instant, the edge fetches from origin once and fans the response back to all thousand. This is why CDNs survive launches that would melt an origin.
Edge proximity matters more than people credit. A round-trip from Mumbai to us-east-1 is ~220 ms; from Mumbai to a Mumbai POP is <10 ms. For a TLS 1.3 handshake (1-RTT) + initial GET (1-RTT), that's the difference between a ~440 ms first byte and a sub-30 ms one. The cache hit rate gets the press; the proximity is what makes the page feel instant.
| CloudFront is good for | CloudFront is bad for |
|---|---|
| Static assets — images, JS, CSS, fonts, video, downloads | Per-user dynamic responses with no reusable cache key |
| API responses with reasonable TTLs (read-mostly GET endpoints) | POST/PUT/DELETE workloads where every response is unique |
| Global apps where origin is in one region but users are everywhere | Single-region apps with all users near the origin (overhead may exceed benefit) |
| Fronting S3 to cut egress costs (S3 → CloudFront is free) | Real-time bidirectional protocols that don't fit HTTP (raw TCP, custom protocols) |
| Edge-terminated TLS with custom domain + ACM certs | Workloads needing strict regional data residency — CloudFront caches globally by default |
2 · The model — distributions, behaviours, the three-tier cache
A distribution is a CloudFront deployment with a unique *.cloudfront.net hostname (and optionally your own CNAME via an ACM certificate). Each distribution has one or more origins (S3, ALB, an arbitrary HTTPS endpoint), and one or more behaviours that route incoming requests to origins based on URL path patterns.
Requests don't go straight to your origin. They flow through three tiers — the nearest edge POP, then a regional edge cache shared across many POPs, then origin. Each tier is a cache; each cache miss falls through to the next.
Everything is cached based on the cache key — by default the URL path, optionally with selected headers, query strings, and cookies. The cache key calculation is deterministic; identical keys always hit the same cache entry within a given POP.
CloudFront billing has two main components: data transfer out (per GB to viewers, tiered) and requests (per 10,000). Egress from S3 to CloudFront is free, so fronting S3 with CloudFront immediately cuts S3-egress charges to zero. Origin Shield is an extra paid layer in front of the regional cache; for high-cardinality origins it can roughly double the coalescing benefit, though most teams won't need it.
3 · The cache key — how a request becomes a lookup
Every incoming request gets reduced to a deterministic hash. Trim or expand what goes into the hash, and you change your hit rate by orders of magnitude.
Three states matter besides hit/miss. Stale-while-revalidate — the cached entry's TTL has passed, but CloudFront serves it anyway while fetching a fresh copy in the background. Controlled by stale-while-revalidate in your origin's Cache-Control. Conditional refresh — on miss, the edge sends If-None-Match with the stored ETag; if origin returns 304, the edge refreshes the TTL without redownloading bytes. Origin error fallback — stale-if-error directs CloudFront to serve a stale entry when origin returns 5xx, turning a brief origin outage into a cache extension instead of a user-facing failure.
4 · Behaviours and cache policies
A behaviour is "for path pattern X, do Y." Y includes which origin to fetch from, which HTTP methods are allowed, what to put in the cache key, TLS version, viewer protocol policy (force HTTPS), and which functions to invoke. Behaviours are evaluated in priority order; the catch-all default behaviour is last.
Cache keys are controlled by cache policies. Three knobs:
- Query strings — none, all, or specific list. Choose carefully — including a unique cache-buster query in the key blows up cache hit rate.
- Headers — same: none, all, or whitelist. Including
Authorizationmeans every signed user gets a separate cache entry (necessary for private content, ruinous if accidental). - Cookies — same. Session cookies in the cache key = no caching, ever. Strip them.
User-Agent to the origin without including it in the cache key — origin sees real UA, cache key stays tight.5 · Lambda@Edge vs CloudFront Functions — the execution-model split
AWS ships two edge-compute primitives that look similar in marketing copy and are radically different in execution. The distinction is worth internalising because each one fails at the other's job.
CloudFront Functions run JavaScript-only on the same machines that serve cached responses — every one of the ~600+ POPs. The runtime is a stripped-down V8 isolate with a custom event-loop-free model: no setTimeout, no network calls, no filesystem, no Promise chain that crosses an I/O boundary, because there is no I/O. The whole function must return synchronously within ~1 ms of CPU. Cold start is effectively zero — the isolate is reused across requests on a single machine. The trade is brutal capability limits in exchange for absurd performance: hundreds of microseconds added per request, sub-cent-per-million-request billing.
Lambda@Edge runs Node.js or Python in regular Lambda execution environments deployed to ~13 regional edge locations (not every POP — the regional edge cache layer). A viewer request that triggers Lambda@Edge first hits the nearest POP, which then proxies to the nearest regional edge location with that function deployed. Cold start is a real Lambda cold start (~50–250 ms for Node.js, longer for Python with deps). Warm invocations add ~5–50 ms. The execution environment is full Linux with network access — you can call DynamoDB, fetch other APIs, decode JWTs against a remote JWKS, run image transformations with sharp. The trade is full power at meaningful latency cost.
| CloudFront Functions | Lambda@Edge | |
|---|---|---|
| Runtime | JavaScript subset (ECMAScript 5.1 + selected later features), single-isolate V8 | Node.js / Python on standard Lambda |
| Where it runs | Every POP (~600+ locations) | ~13 regional edge locations |
| Hooks | Viewer request, viewer response (2 events) | All 4 events incl. origin request/response |
| Cold start | Effectively zero (isolate model) | 50–250 ms typical |
| Warm latency added | < 1 ms | 5–50 ms |
| Max CPU | 1 ms CPU | 5 s viewer / 30 s origin |
| Max size | 10 KB code, 64 KB request | Lambda limits (50 MB zipped) |
| Network | No — pure compute | Yes — can call other APIs |
| Cost | $0.10/M requests | ~$0.60/M requests + duration |
| Reach for it when | Header rewrites, URL normalisation, A/B routing flag, redirect rules, simple HMAC auth | Image resize on the fly, server-side render, OAuth callback handling, signed-URL minting against KMS |
There's a third option worth mentioning: Lambda@Edge with viewer-request trigger is the only way to run code on every request without going to origin. Origin triggers (request/response) only fire on cache misses, which is the cheap default — pay for compute only when the cache doesn't already have the answer.
6 · Private origins — OAC for S3, signed URLs for users
An S3 bucket serving content through CloudFront should not be publicly readable. The pattern is:
- Create an Origin Access Control (OAC) in CloudFront — a SigV4-based identity the distribution uses to fetch from S3. (Replaces the older OAI.)
- Attach a bucket policy to S3 that allows
s3:GetObjectfrom only that OAC, scoped to the distribution ARN. - Disable public access on the bucket completely.
For user-restricted content (paid videos, expiring share links), generate signed URLs or signed cookies from your application using a CloudFront key. Viewer presents the signed URL; CloudFront validates the signature and expiry before serving. Use signed cookies for whole-site auth (all assets under one path); signed URLs for one-off "share this PDF for 24 hours" use cases.
7 · Real-world case studies
Three public stories give a sense of how CloudFront actually shapes systems at consumer scale.
Disney+ — the November 2019 launch. Disney+ went live on 12 November 2019 and within 24 hours had ~10 million subscribers, an event that would have killed any origin sitting unprotected. AWS's published case study on the launch and re:Invent 2020 talk "How Disney+ uses AWS for streaming" describe the architecture: every viewer request — manifest, segment, image, web asset — flows through CloudFront with Origin Shield enabled to ensure the origin (a combination of S3 and Elemental MediaPackage) sees as little duplicated traffic as possible. The interesting detail isn't that they used a CDN — everyone uses a CDN — it's that the entire onboarding flow, including signup forms and license-server lookups, was designed around what could and couldn't be cached at the edge. Static asset cache ratios above 99% kept origin traffic at a fraction of viewer traffic during the surge.
BBC iPlayer — global VOD on a multi-CDN strategy. The BBC has documented its broadcast and streaming infrastructure extensively. The BBC Product & Technology blog describes iPlayer's move to cloud distribution, including how CloudFront handles UK-and-global delivery of on-demand content. The pattern that emerges from their writing: CloudFront fronts an S3 bucket holding HLS/DASH manifests and segments, with Lambda@Edge intercepting viewer requests to do geo-restriction (the BBC's UK-only licensing constraint) and signed-URL validation. The geo check is done at the edge because doing it at origin would mean every play action crossing the Atlantic before a decision.
Hulu / similar streaming services — signed cookies for protected content. Streaming services that monetise per-subscription rather than per-download face a specific CDN problem: how do you let a paying user pull thousands of video segments without re-authenticating, while denying access to non-subscribers? AWS's published reference architecture for this pattern uses CloudFront signed cookies: at login, the application signs a cookie scoped to a path like /videos/* with a 6-hour expiry; the browser sends it on every segment fetch; CloudFront validates the signature and serves from cache. The origin is never touched for individual segments — the entire authorisation happens at the edge. Hulu's engineering team has described variations on this pattern in various talks; the architecture maps directly to the AWS reference.
The through-line: CloudFront's edge proximity buys you latency, but the real lever at consumer scale is using the edge to absorb both traffic (via caching and coalescing) and policy (via signed URLs/cookies and edge compute) so origin only sees a tiny, high-value sliver of requests.
8 · Build it yourself — CloudFront in front of S3 with OAC
- Reuse the S3 bucket from the previous lab, or create one.
BUCKET=lab-cf-$(date +%s) aws s3api create-bucket --bucket $BUCKET --region us-east-1 echo "<h1>Hello from CloudFront</h1>" > /tmp/index.html aws s3 cp /tmp/index.html s3://$BUCKET/index.html - Create an OAC.
OAC_ID=$(aws cloudfront create-origin-access-control --origin-access-control-config '{ "Name": "lab-oac", "OriginAccessControlOriginType": "s3", "SigningBehavior": "always", "SigningProtocol": "sigv4" }' --query 'OriginAccessControl.Id' --output text) - Create the distribution.
cat > /tmp/dist.json <<EOF { "CallerReference": "lab-$(date +%s)", "Origins": { "Quantity": 1, "Items": [{ "Id": "s3-origin", "DomainName": "${BUCKET}.s3.us-east-1.amazonaws.com", "OriginAccessControlId": "${OAC_ID}", "S3OriginConfig": { "OriginAccessIdentity": "" } }]}, "DefaultRootObject": "index.html", "DefaultCacheBehavior": { "TargetOriginId": "s3-origin", "ViewerProtocolPolicy": "redirect-to-https", "AllowedMethods": { "Quantity": 2, "Items": ["GET","HEAD"] }, "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6" }, "Enabled": true, "Comment": "lab" } EOF DIST_ID=$(aws cloudfront create-distribution --distribution-config file:///tmp/dist.json \ --query 'Distribution.Id' --output text) DIST_DOMAIN=$(aws cloudfront get-distribution --id $DIST_ID --query 'Distribution.DomainName' --output text) echo "Distribution domain: $DIST_DOMAIN" - Attach the S3 bucket policy that lets only the distribution fetch.
ACCOUNT=$(aws sts get-caller-identity --query Account --output text) DIST_ARN=$(aws cloudfront get-distribution --id $DIST_ID --query 'Distribution.ARN' --output text) cat > /tmp/bp.json <<EOF { "Version":"2012-10-17","Statement":[{ "Effect":"Allow","Principal":{"Service":"cloudfront.amazonaws.com"}, "Action":"s3:GetObject","Resource":"arn:aws:s3:::${BUCKET}/*", "Condition":{"StringEquals":{"AWS:SourceArn":"$DIST_ARN"}} }]} EOF aws s3api put-bucket-policy --bucket $BUCKET --policy file:///tmp/bp.json - Wait for deployment (~5-10 min), then hit it.
aws cloudfront wait distribution-deployed --id $DIST_ID curl -I https://$DIST_DOMAIN/index.html # x-cache: Miss from cloudfront (first request) # Hit it again: curl -I https://$DIST_DOMAIN/index.html # x-cache: Hit from cloudfront - Tear down.
aws cloudfront get-distribution-config --id $DIST_ID --query 'DistributionConfig' > /tmp/dist-cfg.json ETAG=$(aws cloudfront get-distribution-config --id $DIST_ID --query 'ETag' --output text) jq '.Enabled = false' /tmp/dist-cfg.json > /tmp/dist-cfg-off.json aws cloudfront update-distribution --id $DIST_ID --if-match $ETAG --distribution-config file:///tmp/dist-cfg-off.json aws cloudfront wait distribution-deployed --id $DIST_ID ETAG2=$(aws cloudfront get-distribution-config --id $DIST_ID --query 'ETag' --output text) aws cloudfront delete-distribution --id $DIST_ID --if-match $ETAG2 aws cloudfront delete-origin-access-control --id $OAC_ID --if-match $(aws cloudfront get-origin-access-control --id $OAC_ID --query ETag --output text) aws s3 rm s3://$BUCKET --recursive aws s3api delete-bucket --bucket $BUCKET
cloudfront wait; just be patient. Disabling, then deleting, is a two-step process — the distribution must be Disabled and Deployed before delete will succeed.9 · What breaks
- "Origin returns 403 even though I set the bucket policy." The bucket policy uses the new OAC condition; old guides show the legacy OAI condition. Mixing the two doesn't work — use OAC for new distributions.
- Low cache hit rate. Cache key includes too much — auth headers, session cookies, random query params. Inspect
CacheHitRatein CloudFront reports; trim the cache key with a custom cache policy. - Cache invalidations are expensive and slow. First 1,000 invalidation paths/month are free, then $0.005 each. Worse, propagating an invalidation to all ~600+ POPs takes 5–10 minutes — you cannot use invalidation as a synchronous "purge cache now" primitive. The right answer for deploys is content-addressed asset names (
app.{sha}.js) so old and new coexist; invalidate only HTML. - Global propagation delay on distribution config changes. Distribution updates (new behaviours, cert changes, OAC) take 5–10 minutes to deploy worldwide. The
aws cloudfront wait distribution-deployedcommand exists for this; don't run CI deploys that assume distribution changes are instant. - Stale content after a deploy. Cache TTL is independent of viewer expectations; if the origin sent
max-age=86400, CloudFront keeps it for a day regardless of when you redeployed. Send shortermax-agefrom origin, set a max TTL on the cache policy, or use content-hashed URLs. - CloudFront Functions JavaScript subset surprises. No
async/awaitthat crosses I/O (because there is no I/O), noPromisechains that depend on timers, no modern crypto API (must use the limitedcryptomodule), no top-levelimport. Errors are particularly cryptic — most failures look likeThe function failed compilationwith no stack. Test in CloudFront's test runner before deploying. - Origin response timeouts. Default origin response timeout is 30 seconds. Long-running APIs (some report generators, heavy SSR pages) hit this and return a 504 to the viewer even when origin is still working. Raise to up to 60s (custom origin) or design the slow path to be async.
- Edge location coverage varies by region. The "~600 POPs" number is global but unevenly distributed. Some metros have multiple POPs; some emerging markets have none nearby, so requests route to a more distant POP. Check the edge-location list against your real user geography rather than assuming uniform coverage.
- SNI vs dedicated IP for older clients. CloudFront defaults to SNI-based HTTPS, which means the client must send the requested hostname during the TLS handshake. Browsers from before about 2014 (legacy Android, old IE) can't, and will see cert errors. AWS sells a dedicated-IP option (~$600/month per distribution) to support these clients — rarely worth it unless you have a compliance reason.
- "My WebSocket doesn't work." Enable HTTP/2 + HTTP/3 on the distribution; WebSocket support requires it (and the right origin protocol policy that forwards
Upgradeheaders). - Pricing surprises on data transfer to viewers. CloudFront egress is cheaper than EC2 egress but is still your biggest line item at scale. Asia-Pacific and South America rates are roughly 2× North America. Use AWS's tiered pricing to model real costs; volume discounts only kick in above 10 TB/month.
10 · Further reading
- CloudFront developer guide. The canonical reference. Skim the "best practices for security" page and the cache-policy reference at minimum.
- Disney+ on AWS. AWS's published case study on the launch architecture. Watch the re:Invent 2020 session for the technical depth.
- BBC iPlayer in the cloud. The BBC's engineering blog on moving VOD onto cloud-based delivery, CloudFront among them.
- Serving private content with CloudFront + Lambda@Edge. AWS's reference architecture for signed cookies and edge-side authorisation — the pattern most paid streaming services use.
- Validating JWTs with CloudFront Functions. Concrete example of the CF Functions execution model in action.
- How CDNs work. The protocol-and-cache primer.
- S3 internals. The most-common CloudFront origin.