---
title: "14 — CloudFront: Putting My Content Closer to the User"
uid: cloudfront-cdn
tags: ["aws", "cloudfront", "invalidation", "roadmap:aws", "cdn", "edge", "caching"]
excerpt: "CloudFront is a global cache in front of your origin, and the operational art is three controls: distributions (shape), policies (cache key + access), and invalidations (forcing it to forget)."
date: 2026-08-13T03:28:28+0000
source: https://www.aveshina.my.id/en/blog/cloudfront-cdn
---

"A CDN, like any other" was how I filed CloudFront, which meant I had no vocabulary for the levers it actually exposes. The controls that registered: **CloudFront is a global cache that sits in front of my origin (an S3 bucket, an ALB, any HTTP endpoint) and serves requests from whichever edge location is closest to the user, and the operational art is three controls — distributions (the cache's shape), policies (what defines the cache key and who can access what), and invalidations (forcing the cache to forget)** [1]. Once those three were distinct, the CDN stopped being a black box.

## The core trick: edge locations

CloudFront's network is a fleet of **edge locations** — small Points of Presence spread across the world, far more numerous than AWS regions. When a user requests a file, the request lands at the nearest edge location. If that edge has the file cached (a **cache hit**), it returns immediately — fast, because the bytes come from nearby. If it doesn't (a **cache miss**), CloudFront fetches from the **origin** I configured, returns the file to the user, and keeps a copy at the edge so the next request is a hit.

The win is twofold: **latency drops** because the user gets bytes from a nearby edge instead of a single distant origin, and **origin load drops** because most requests never reach it. The same pattern every cache uses; the special part is the global edge footprint.

## Distributions: the cache's shape

A **distribution** is the CloudFront configuration unit — it tells CloudFront what to serve and where to get it [2]. Two flavors:

- **Web distribution** — for websites, APIs, static assets. The default; almost everything is this.
- **RTMP distribution** — for legacy media streaming. Largely historical.

A web distribution binds together: one or more **origins** (the S3 bucket, the ALB), **behaviors** (rules mapping URL paths to origins and to cache settings), and the global settings (price class, TLS certificate, alternate domain names). The way of thinking: a distribution is a routing-and-caching configuration that points at my real backend.

## Cache policies: the cache key

The single most important CloudFront concept is the **cache key** — the set of inputs that determines whether two requests are treated as the same cached object. If the key includes only the URL, then /image.jpg is cached once for everyone. If the key also includes a header or a query string, those become part of the lookup.

**Cache policies** (and **origin request policies**) are AWS's structured way of controlling this [3]:

- **Cache policy** — what goes into the cache key. Tighten it (URL only) to maximize hit ratio; widen it (include device header, query string) to vary the cached response.
- **Origin request policy** — what gets forwarded to the origin on a miss. Even when the cache key is tight, I might want the origin to see a header for analytics or auth.

The classic mistake: putting too much in the cache key, which fragments the cache and tanks the hit ratio. The discipline is to include only what actually changes the response — usually the URL, sometimes a Cookie or Accept-Language, rarely more.

## Access control: who can reach what

CloudFront layers access control on top of caching [3]:

- **Origin Access Control (OAC)** — for S3 origins, makes the bucket private and lets only CloudFront read from it. Users can never bypass CloudFront to hit the bucket directly.
- **Signed URLs / signed cookies** — for premium content. CloudFront signs a time-limited token so only authorized users fetch certain paths.
- **AWS WAF + geo restrictions** — a web application firewall and country-level allow/deny lists at the edge.

The pattern: keep the origin (S3, ALB) private to CloudFront, and do all access decisions at the edge. That way the cache is also a security boundary.

## Invalidations: forcing the cache to forget

CloudFront caches objects until their **TTL** expires. If I deploy a new version of app.js before the TTL, the edges keep serving the old one — that's the cache doing its job, but it's also a problem. An **invalidation** tells CloudFront to drop specific paths from its edge caches immediately, forcing the next request to re-fetch from origin [4].

```
aws cloudfront create-invalidation --distribution-id ABCD1234XYZ --paths "/app.js" "/assets/*"
```

The cost trade-off: invalidations are billable after a free monthly allowance, and broad invalidations (/*) can be expensive and hammer the origin. The better pattern is **content-addressed filenames** — app.<hash>.js — so a new deploy is a *new path* the cache has never seen, and the old path simply expires. Invalidations are the fallback for when I can't change the filename.

```figure
<svg viewBox="0 0 680 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A user close to a CloudFront edge location gets a cache HIT (fast return); a user far from the origin triggers a MISS that fetches from S3, then back-fills the edge. A TTL clock and an invalidation broom sit beside the cache, representing the two ways cached objects leave.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <defs><marker id="cfa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker></defs>

    <!-- user 1 -->
    <rect x="20" y="50" width="80" height="36" rx="8" fill="#e0e7ff" stroke="#6366f1"/><text x="60" y="73" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">user (EU)</text>

    <!-- edge -->
    <rect x="200" y="40" width="160" height="180" rx="10" fill="#dcfce7" stroke="#16a34a"/>
    <text x="280" y="62" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">CloudFront edge</text>
    <rect x="220" y="78" width="120" height="32" rx="5" fill="#fef9c3" stroke="#ca8a04"/><text x="280" y="98" font-size="9" font-weight="700" fill="#422006" text-anchor="middle">HIT → return fast</text>
    <rect x="220" y="120" width="120" height="32" rx="5" fill="#fee2e2" stroke="#dc2626"/><text x="280" y="140" font-size="9" font-weight="700" fill="#7f1d1d" text-anchor="middle">MISS → fetch origin</text>
    <text x="280" y="178" font-size="9" font-style="italic" fill="#052e16" text-anchor="middle">TTL clock</text>
    <text x="280" y="196" font-size="9" font-style="italic" fill="#052e16" text-anchor="middle">invalidation broom</text>

    <!-- arrows user -> edge -->
    <path d="M100,68 L200,68" stroke="#16a34a" stroke-width="1.5" marker-end="url(#cfa)"/>

    <!-- origin -->
    <rect x="510" y="120" width="140" height="50" rx="8" fill="#fce7f3" stroke="#db2777"/><text x="580" y="142" font-size="11" font-weight="700" fill="#500724" text-anchor="middle">Origin (S3 / ALB)</text><text x="580" y="158" font-size="9" fill="#500724" text-anchor="middle">source of truth</text>

    <!-- MISS arrow -->
    <path d="M360,140 L510,140" stroke="#dc2626" stroke-width="1.3" stroke-dasharray="4 3" marker-end="url(#cfa)"/>
    <text x="435" y="133" font-size="9" fill="#7f1d1d" text-anchor="middle">on miss</text>
  </g>
</svg>
```

## How I use this

CloudFront is the front door for almost everything I serve to users — static assets, single-page apps, even APIs where caching is sane. My standing setup: an S3 origin for static assets with OAC so the bucket is private to CloudFront; content-addressed filenames so deploys never need invalidations; a tight cache policy (URL-only key for static, carefully widened for APIs) to keep the hit ratio high; and the TLS certificate on the distribution so the whole thing is HTTPS. For dynamic APIs I use CloudFront more for its edge-to-origin network path and WAF than for caching, with cache policies that pass everything through. The mental test for any CloudFront change is the same as for any cache: *what does the cache key include, and what happens when the cached thing changes?* Answering those two up front prevents most of the cache-related incidents I've ever caused.

## References

[1] Amazon Web Services, "What is Amazon CloudFront?," 2024. [Online]. Available: [https://aws.amazon.com/cloudfront/](https://aws.amazon.com/cloudfront/)

[2] Amazon Web Services, "Working with distributions," CloudFront Developer Guide, 2024. [Online]. Available: [https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html)

[3] Amazon Web Services, "Security and IAM in CloudFront," CloudFront Developer Guide, 2024. [Online]. Available: [https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/security-iam.html](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/security-iam.html)

[4] Amazon Web Services, "Invalidating files," CloudFront Developer Guide, 2024. [Online]. Available: [https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html)

```quiz
Q: What does a CloudFront cache HIT mean?
- The requested object was already at the nearest edge location, so it returns immediately without hitting the origin
- The object was purged from the cache
correct: 0
explain: On a hit the edge serves the cached copy — low latency, no origin traffic. On a miss the edge fetches from origin, returns it, and caches it.

Q: The cache key is best kept tight (URL only) because…
- widening it with extra headers/query strings fragments the cache and lowers the hit ratio
- AWS charges per key field
correct: 0
explain: Everything in the cache key makes a separate cached variant. Include only what actually changes the response, or the cache balloons and the hit ratio collapses.

Q: You deploy a new app.js but the edges keep serving the old one. The two correct responses are…
- wait for the TTL to expire, or run an invalidation (better: use content-addressed filenames so it's a new path)
- delete the distribution
correct: 0
explain: The cache is doing its job. Either let the TTL elapse or invalidate the path. Content-addressed filenames avoid the problem entirely by making each deploy a new URL.

Q: Origin Access Control (OAC) on an S3-backed distribution lets me…
- keep the bucket private so only CloudFront can fetch from it, preventing users bypassing the CDN
- compress the objects
correct: 0
explain: OAC makes the S3 origin readable only by CloudFront. Users must go through the edge, which becomes both the cache and the access-control layer.

Q: Why route an API through CloudFront even when its responses aren't cacheable?
- for the edge-to-origin network path, TLS termination, WAF attachment, and geographic restrictions
- because APIs cannot be served from ALB directly
correct: 0
explain: For dynamic APIs, CloudFront's value is its global network, security attachments (WAF, geo), and TLS — not caching. The cache policy passes requests through to the origin.
```
