# CDNs and Object Storage: Cache Keys, Invalidation and Signed URLs (2026)

> The production half of CDNs and object storage: which request parts belong in the cache key, when an origin shield pays off, what purging really costs, signed URLs and cookies, a cost model with worked numbers, and failing over to a second region.

*Source: https://www.infowok.com/cdn-object-storage-follow-ups/ · Navmeet Kaur · Published October 24, 2026*

---

The CDN is in place and the first week goes well. Then the incidents start. A marketing link adds `?utm_source=` to every URL and the hit ratio halves. A deploy purges everything and the origin falls over at nine in the morning. A paid video leaks because one URL never expired. The origin region has a bad hour and the site shows errors.

None of that changes the core idea, yet all of it decides whether the setup survives production. This lesson is the second half of [CDNs and object storage](/cdn-object-storage/), which covers what each part is and the hit-ratio arithmetic.

<Prerequisites>

- [CDNs and object storage](/cdn-object-storage/): the origin, the edge, hit ratio, TTLs and versioned file names.
- [Design a file storage service](/design-a-file-storage-service/): presigned and multipart uploads, which this lesson only touches.

</Prerequisites>

<Callout type="note" title="Remember from the core lesson">
With a high hit ratio, why can users still complain about speed? The few misses take the slow path, so the p99 latency is the miss latency. The average hides the tail.
</Callout>

<KeyTakeaways>

- Put a value in the cache key only if it changes the response. Every extra value multiplies the copies and cuts the hit ratio.
- A shield merges misses from many edges, and it is billed, so use it when many edges ask for the same large file.
- Version names instead of purging. A wildcard purge empties the cache and sends the full load to the origin.
- Signed URLs fit one file and signed cookies fit many. Keep the bucket private behind the CDN.
- Count the bill as storage plus requests plus egress, and see which layer pays the egress.
- Failover between regions is only as fresh as the replication behind it.

</KeyTakeaways>

<LessonOutcome>

Tune a CDN for production: choose the cache key, decide on a shield, plan a deploy without a purge storm, protect private files, estimate the monthly bill and design a regional failover, then answer the usual follow-ups in a line each.

</LessonOutcome>

## What Belongs in the Cache Key?

By default, [CloudFront's cache key](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/understanding-the-cache-key.html) is the domain and the URL path. Everything else is ignored until you add it, and every value you add splits the cache.

![Four requests for the same logo that differ only by a tracking query string, a User-Agent header and a session cookie: with those in the cache key the edge stores four copies and misses four times, with only host and path in the key it stores one copy and three of the four requests hit](./cdn-object-storage-follow-ups-cachekey.svg)

AWS gives a concrete case. A viewer's `Accept-Language` header can arrive as `en-US,en`, `en,en-US`, `en-US, en` or `en-US`, all meaning English, so keying on it can store the same file up to four times. A better design puts the language in the URL (`/en-US/story.html`). `User-Agent` has thousands of variants, and session cookies can be unique per viewer, so AWS calls both poor candidates for the key.

If the origin needs a value only for logging or analytics, forward it with an origin request policy and keep it out of the key.

<Callout type="warning" title="The reverse mistake">
Leaving out a value that does change the response serves one person's variant to everyone. That is the root of web cache poisoning: a request part the cache ignores, the *unkeyed input*. [PortSwigger's guidance](https://portswigger.net/web-security/web-cache-poisoning) is to cache only static responses and to disable request headers the site does not need. Per [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), a response with a `Vary` header may be reused only when the named request headers match.
</Callout>

## When Does an Origin Shield Pay Off?

Without a shield, 20 edges can each ask the origin for the same new file. An origin shield sends every edge miss through one place, so AWS says requests "are consolidated ... resulting in as few as one request going to your origin".

Assume 90 percent hits at the edge and 70 percent at the shield, as in the core lesson: that leaves ≈3 percent of traffic for the origin.

The cost side matters too. AWS bills the shield per request, and says it is a poor fit for [dynamic, low-cacheability or rarely requested content](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html). A `GET` with a TTL under 3,600 seconds counts as dynamic, so the shield is an extra billed hop on every such request.

| Use a shield when | Skip it when |
|---|---|
| Viewers are spread across many regions | Almost all viewers are near the origin |
| The origin is expensive per request (resizing, packaging) | Files are small and the origin is cheap |
| Popular files are large | Content is mostly dynamic or short-lived |

Also watch for *request collapsing*: several simultaneous misses for one file wait on a single origin fetch. Whether your CDN does this by default is worth checking, since it is what stops a popular file's expiry from becoming a stampede.

## What Does Invalidation Really Cost?

Money first. [CloudFront gives 1,000 invalidation paths free per month](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PayingForInvalidation.html) across the account, then bills per path. A path with a `*` wildcard counts as one path however many files it covers. Check the pricing page for the current rate. Assume a deploy changes 300 files and you purge each one, four deploys a month:

```text
Per file: 300 × 4 = 1,200 paths
 free 1,000 → 200 billed
Wildcard /assets/*: 4 paths, free
```

The wildcard looks cheaper, but it has a hidden cost. Assume 50,000 hot files and 20 edge caches. After `/*`, every edge must refill, so the origin can see up to 50,000 × 20 ≈ 1,000,000 requests in a burst. Behind a shield it sees ≈50,000, one per file. Both numbers are assumptions that show the shape.

Time and reach matter as well. A purge is not instant, and it cannot reach copies already in browsers or office proxies, which is why [AWS recommends versioned file names](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html) for anything updated often. Tag-based invalidation, where many objects share a tag and one call drops them all, exists on some CDNs, and CloudFront counts one tag as one path.

## How Do Signed URLs and Cookies Protect Files?

A private file needs two locks. The bucket must refuse direct requests, and the CDN must check each viewer. For the first, CloudFront [origin access control](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) signs the CDN's requests so the bucket can stay private. For the second, AWS [describes two tools](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-choosing-signed-urls-cookies.html):

| Tool | Best for | Trade-off |
|---|---|---|
| Signed URL | One file, such as a download | A new URL per file |
| Signed cookie | Many files, such as every segment of a video | Needs a client that keeps cookies |

A signed URL is a bearer token: whoever holds it can use it until it expires, so sign for minutes, not days. Use signed cookies for a whole video, because signing each of hundreds of segments is wasteful. One trap: signed query parameters (`Signature`, `Expires`, `Policy`) must not be part of the cache key, or every viewer's URL is a unique key and nothing is ever a hit. Confirm this in your CDN's documentation.

<Callout type="note" title="Uploads are a different path">
A presigned S3 URL goes straight to the bucket, [valid for up to 7 days with SDK signing](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html), and never touches the CDN. Chunking, resuming and multipart limits are in [Design a file storage service](/design-a-file-storage-service/), so this lesson does not repeat them.
</Callout>

## What Does the Bill Look Like?

Three meters run: storage, requests and egress. Egress is usually the big one, and it changes by layer. [S3 does not charge](https://aws.amazon.com/s3/pricing/) for data sent to CloudFront, and [Cloudflare R2 states](https://developers.cloudflare.com/r2/pricing/) that egress from R2 is free, with storage at $0.015 per GB-month and $0.36 per million Class B (read) operations. Prices change, so check the page on the day you decide.

A worked month, with every rate an assumption: deliver 100 TB (100,000 GB) of 400 KB files, which is 250 million requests, at a 95 percent hit ratio.

| Path | Calculation | Monthly |
|---|---|---|
| Direct from origin | 100,000 GB × $0.09 | $9,000 |
| Through a CDN | 100,000 GB × $0.05 | $5,000 |
| Origin reads on misses | 12.5M × $0.0004 / 1,000 | $5 |

The `$0.0004` per 1,000 GET figure is S3's published rate for one region, and the two per-GB rates are assumptions. The saving of ≈$4,000 comes from the lower delivery rate, while the origin reads are tiny at a high hit ratio.

Per-request CDN fees are left out, so treat this as a shape, not a quote. On a zero-egress store, reading 250 million objects directly at $0.36 per million would be ≈$90. The best layer to pay for changes with the provider.

## What If the Origin Region Fails?

![The viewer asks the edge, which tries the primary bucket in region A first and the secondary bucket in region B on an error; an asynchronous copy from A to B means the newest objects may be missing in B for a few minutes](./cdn-object-storage-follow-ups-failover.svg)

Failover in CloudFront uses [origin groups](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html), which try a primary origin and then a secondary on chosen status codes (any of 400, 403, 404, 416, 429, 500, 502, 503, 504), only for `GET`, `HEAD` and `OPTIONS`. By default it tries the primary for up to 30 seconds (3 attempts of 10 seconds). Cut the connection timeout to 2 seconds with one attempt and failover starts in about 2 seconds, an assumption to tune against flapping.

The secondary is a replica, only as fresh as the copy.

[S3 Replication Time Control](https://docs.aws.amazon.com/AmazonS3/latest/userguide/replication-time-control.html) replicates 99.9 percent of objects within 15 minutes, so plan for new objects to be absent for that long. This is the consistency stance: the primary is strongly consistent, the replica is eventually consistent, and the edge may add TTL staleness on top.

Versioned, immutable names make failover safe. An old file is identical in both regions, and only a brand-new key can be missing. Do not fail over on `404` unless you want every genuine missing file to hit the secondary too.

## Practice: Fix a 40 Percent Hit Ratio

<Exercise title="Why is the hit ratio only 40 percent?" time="15 min">

A static site on a CDN shows a **40 percent** hit ratio. The cache key includes **all cookies**, the **User-Agent** header and **all query strings**. Pages carry `?utm_campaign=` tags. CSS and JS have the same name on every deploy, with `max-age=60`.

1. List the changes that raise the ratio, in order.
2. What should the CSS and JS headers be?
3. After the fix, how do you ship a new `app.js`?

<Hint>

Ask of each key value: does it change the bytes? Then ask how often a file really changes.

</Hint>

<Expected title="One reasonable answer">

```text
1. Key = host + path only.
   Drop cookies and User-Agent.
   Allow-list the query strings
   that change output; ignore
   utm_*.
2. Hash the names: app.3f9a.js
   max-age=31536000, immutable
3. Build a new hash, update the
   HTML, deploy. No purge.
```

Only HTML needs a short TTL, so it points to new hashed assets. Forward the cookies the origin needs through an origin request policy, not the key. Measure origin request rate before and after, and watch for any page that really varies by cookie.

</Expected>

</Exercise>

## What Would You Watch in Production?

Track five signals: edge hit ratio and the origin request rate, bytes served from the origin, origin 5xx rate, `4xx` rate (a spike can mean a leaked or expired link) and purge counts. CloudFront logs mark shield hits as `OriginShieldHit`, so you can tell where a request stopped. Assume an SLO of 99.9 percent of requests under 300 ms, and tune it.

<InInterview>

Expect follow-ups. Answer each with a trade-off, not a product name:

| They ask | Answer in one line |
|---|---|
| Hit ratio dropped after a release? | Check the key: new cookie, header or query string |
| Purge everything on deploy? | No. Version names; a wildcard refill hits the origin |
| Do you need a shield? | Many regions and a costly origin, yes |
| Paid video links leak? | Private bucket, short signed cookies |
| One file, many users? | Signed URL |
| Region down? | Origin group; replica lags up to 15 minutes |
| Cost too high? | Raise hit ratio, shrink objects, check which layer bills egress |
| Personal data? | Skip the shared cache |

Level expectations: a mid-level answer names the CDN and the hit ratio. A senior answer adds the key, versioned names, signed access and the failure path. A staff answer adds cost per layer, the replication lag in the failover and how the rollout is measured.

</InInterview>

## Check Yourself

<CheckYourself>

<Question q="Why is the Accept-Language header a risky cache key value, and what is the safer design?">

Equivalent values such as `en-US,en` and `en,en-US` are stored as separate copies, so the same file can be cached several times. Put the language in the URL path instead and keep the header out of the key.

</Question>

<Question q="A deploy purges /* and the origin slows down. What happened and how do you avoid it?">

Every edge emptied at once and refilled from the origin. Version asset names so no purge is needed, and put a shield in front so the refill is one request per file.

</Question>

<Question q="Which do you use for a paid course with 400 video segments per lesson, and why?">

A signed cookie, because it covers many files without changing URLs. Keep the bucket private behind origin access control and set a short expiry.

</Question>

<Question q="After failover, a photo uploaded two minutes ago returns 404. Why, and is it a bug?">

Replication is asynchronous, so objects are copied within 15 minutes for 99.9 percent of them, so the new key may not exist in the secondary yet. It is expected, so design the client to retry or show a pending state.

</Question>

</CheckYourself>

## In One Picture

```text
Key       only what changes bytes
Shield    many edges, dear origin
Change    new name, not a purge
Private   private bucket + signing
Bill      storage+requests+egress
Failover  replica lags; keep names
          immutable
```

## Conclusion

The core design is two boxes. The production answer is everything around them: a tight cache key, a shield only where it pays, file names that change instead of purges, private buckets with short-lived signing, a bill you can predict and a failover that admits its lag. Say which trade-off you took each time and the follow-ups get easy.

**Which of these has caught your team out: the cache key, a purge, or a leaked link?** Tell us in the comments.

<NextSteps>

- **Back to the core lesson:** [CDNs and object storage](/cdn-object-storage/) for hit-ratio arithmetic, durability and failure modes.
- **Uploads and sync:** [Design a file storage service](/design-a-file-storage-service/).
- **Video at scale:** [Design a video streaming service](/design-a-video-streaming-service/), where signed cookies and chunked delivery come together.

</NextSteps>
