Object storage and CDNs
Photos, videos, backups, logs and data lakes all end up in object storage, and anything users download a lot goes out through a CDN. The products look alike from a distance, but they differ in limits, storage classes, what you pay to read data out, and how their caches fill and purge. This guide explains how they work, how they differ, and which to choose, with a chooser and code you can run.
- Sam Where do photos and videos go?
- Priya Into object storage, and out through a CDN. The products look alike, but differ in limits, storage classes, what you pay to read data out, and how their caches fill and purge.
- Sam What does each half do?
- Priya Object storage keeps files durably and cheaply, addressed by bucket and key. A CDN keeps copies of popular content near users, so downloads are fast and the origin is spared. Object storage isn't a file system, and a CDN serves stale content until it expires.
- Sam Walk me through an upload and a download.
- Priya The app asks the API for a presigned URL, then uploads the file straight to the bucket, in parts if it's large. The first viewer's request misses at the edge and at the shield, which fetch it from the bucket. Later views are cache hits. And an edited photo gets a new URL, so no purge is needed.
- Sam What are the options?
- Priya Amazon S3, with objects up to fifty terabytes since December twenty twenty-five, strong consistency, and thousands of requests a second per prefix. Google Cloud Storage, with dual-region buckets. Azure Blob Storage, with hot, cool, cold and archive tiers. Cloudflare R2, with no egress fees. And the CDNs: CloudFront, Cloud CDN and Media CDN, Azure Front Door, whose classic CDN retires in September twenty twenty-seven, and Cloudflare, Fastly and Akamai.
- Sam How do I choose?
- Priya Store files in your cloud's object store and keep metadata in a database. Upload directly with presigned URLs. Serve popular content through a CDN with a shield and versioned URLs. Move cold data down with lifecycle rules. And if downloads dominate the bill, compare an egress-free store.
- Priya Your turn. You add more CDN edges and the origin gets busier. Why?
- Priya Each edge sees less traffic, so its copies expire before they're reused. A shield layer fixes it.
- Sam And the code?
- Priya Ten images through three and then twelve edges, with and without a shield, plus the arithmetic for multipart upload parts.
- Sam Common mistakes?
- Priya Uploads through your app servers. Purging instead of versioning. Public buckets for private files. Forgetting egress. And archive tiers for data you actually read.
1. Two halves of one job
Module 10 teaches the concepts; this guide is about the products.
| Object storage | CDN | |
|---|---|---|
| Job | Keep files (objects) durably and cheaply, addressed by bucket and key | Keep copies of popular content near users, so downloads are fast and the origin is spared |
| Unit | An object: bytes plus metadata, written whole | A cached response, kept for a time to live |
| Strength | Practically unlimited size, very high durability | Low latency worldwide, absorbs huge traffic |
| Limit | Not a file system: no in-place edits or renames of parts of an object | Serves stale content until it expires or is purged |
2. How an upload and a download work
The usual pattern: clients upload straight to the bucket with a presigned URL, and downloads go through a CDN with a shield layer in front of the bucket.
3. The products
Amazon S3
Objects up to 50 TB since December 2025 (uploaded in parts); strongly consistent reads after writes; at least 3,500 writes and 5,500 reads a second per partitioned prefix, scaling with more prefixes. Storage classes from Standard through Infrequent Access to the Glacier archive tiers, plus S3 Express One Zone for single-digit-millisecond access in one zone.
Best for: the default object store on AWS, and the API most others copy.
Cloud Storage
Strongly consistent, with Standard, Nearline, Coldline and Archive classes, and regional, dual-region or multi-region buckets. Autoclass can move objects between classes by access.
Best for: object storage on Google Cloud; dual-region buckets for data that must survive a region loss.
Azure Blob Storage
Containers of blobs with hot, cool, cold and archive access tiers, and lifecycle rules to move data between them. Block blobs for files, append blobs for logs.
Best for: object storage on Azure.
Cloudflare R2
An S3-compatible object store that charges no egress (data transfer out) fees, which changes the cost of serving large files to many users.
Best for: download-heavy content where egress would dominate the bill.
Amazon CloudFront
AWS's CDN, with an optional Origin Shield layer, CloudFront Functions and Lambda@Edge for logic at the edge, and signed URLs or cookies for private content.
Best for: content served from AWS origins.
Cloud CDN and Media CDN
Cloud CDN caches content behind Google's global external Application Load Balancer; Media CDN runs on the infrastructure used for YouTube, for large video and download traffic.
Best for: content served from Google Cloud; Media CDN for streaming at scale.
Azure Front Door
Microsoft's global edge for caching and routing, with a web application firewall. Azure CDN Standard from Microsoft (classic) retires on 30 September 2027; Front Door Standard and Premium are the replacement.
Best for: content and web apps on Azure.
Cloudflare, Fastly, Akamai
CDNs that work with any origin. Cloudflare bundles security and edge compute; Fastly is known for very fast purges and programmable caching; Akamai has one of the largest edge networks.
Best for: multi-cloud origins, or when edge features and purge speed matter.
4. Differences at a glance
| Option | Type | Consistency or freshness | Cold or archive tier | Notable |
|---|---|---|---|---|
| S3 | Object store | Strong read-after-write | Glacier classes | 50 TB objects; Express One Zone |
| Cloud Storage | Object store | Strong | Coldline, Archive | Dual-region buckets, Autoclass |
| Azure Blob | Object store | Strong | Cold, archive tiers | Append blobs, lifecycle rules |
| R2 | Object store | Strong | Infrequent access class | No egress fees |
| CloudFront | CDN | TTL, invalidations | n/a | Origin Shield, edge functions |
| Cloud CDN / Media CDN | CDN | TTL, invalidations | n/a | Behind the global load balancer |
| Front Door | CDN and edge routing | TTL, purge | n/a | WAF built in; classic CDN retiring |
| Cloudflare / Fastly / Akamai | CDN | TTL, purge (Fastly's is very fast) | n/a | Any origin, edge compute |
5. When to choose which, and why
- Store files in your cloud's object store (S3, Cloud Storage, Blob). Keep metadata (owner, size, status) in a database and only the bytes in the bucket.
- Upload directly from clients with presigned (signed) URLs, in parts for anything large, so your servers never carry the bytes.
- Serve popular content through a CDN, with a shield layer when there are many edges, and versioned URLs (
logo.v42.svg) so you rarely need purges. - Move cold data to cheaper classes with lifecycle rules; remember archive tiers charge for and delay retrieval.
- If egress dominates the bill (large downloads to many users), compare an egress-free store such as R2, or negotiated CDN pricing.
Try it: choose for your requirements
6. CDN edges and an origin shield, in code
One request a second for five minutes, for ten images, with a 60-second TTL. With three edges, half the requests hit the edge cache. With twelve edges, each edge sees so little of the traffic that its copies expire before they're reused: no hits at all, and every request goes to the origin. A shield layer between the edges and the origin keeps origin fetches at 50 either way (ten images, refreshed every minute). The last line works out multipart upload parts. The program was run in all three languages; each prints the output below it.
import math
TTL = 60 # seconds an edge or shield may serve a cached copy
class Cache:
def __init__(self):
self.expires = {}
def fresh(self, obj, now):
return self.expires.get(obj, -1) > now
def store(self, obj, now):
self.expires[obj] = now + TTL
def run(edges, use_shield):
"""One request a second for 300 s, spread over the edges, for 10 objects."""
edge_caches = [Cache() for _ in range(edges)]
shield = Cache()
edge_hits = origin = 0
for t in range(300):
obj, edge = f"img{t * 7 % 10}", edge_caches[t % edges]
if edge.fresh(obj, t):
edge_hits += 1
continue
if use_shield and shield.fresh(obj, t):
edge.store(obj, t) # filled from the shield, not the origin
continue
origin += 1
if use_shield:
shield.store(obj, t)
edge.store(obj, t)
return edge_hits, origin
for edges in (3, 12):
hits, without = run(edges, False)
_, with_shield = run(edges, True)
print(f"{edges} edges: edge hit ratio {hits / 3:.0f}%, origin fetches {without} without a shield, {with_shield} with one")
# Multipart upload: S3 allows up to 10,000 parts, each 5 MiB to 5 GiB (the last part may be smaller).
size_gib = 200
part_mib = max(5, math.ceil(size_gib * 1024 / 10_000))
print(f"a {size_gib} GiB upload needs parts of at least {part_mib} MiB: {math.ceil(size_gib * 1024 / part_mib)} parts")
const TTL = 60; // seconds an edge or shield may serve a cached copy
class EdgeCache {
expires = new Map<string, number>();
fresh(obj: string, now: number): boolean {
return (this.expires.get(obj) ?? -1) > now;
}
store(obj: string, now: number): void {
this.expires.set(obj, now + TTL);
}
}
// One request a second for 300 s, spread over the edges, for 10 objects.
function run(edges: number, useShield: boolean): [number, number] {
const edgeCaches = Array.from({ length: edges }, () => new EdgeCache());
const shield = new EdgeCache();
let edgeHits = 0, origin = 0;
for (let t = 0; t < 300; t++) {
const obj = `img${(t * 7) % 10}`, edge = edgeCaches[t % edges];
if (edge.fresh(obj, t)) { edgeHits++; continue; }
if (useShield && shield.fresh(obj, t)) { edge.store(obj, t); continue; } // filled from the shield, not the origin
origin++;
if (useShield) shield.store(obj, t);
edge.store(obj, t);
}
return [edgeHits, origin];
}
for (const edges of [3, 12]) {
const [hits, without] = run(edges, false);
const [, withShield] = run(edges, true);
console.log(`${edges} edges: edge hit ratio ${(hits / 3).toFixed(0)}%, origin fetches ${without} without a shield, ${withShield} with one`);
}
// Multipart upload: S3 allows up to 10,000 parts, each 5 MiB to 5 GiB (the last part may be smaller).
const sizeGib = 200;
const partMib = Math.max(5, Math.ceil((sizeGib * 1024) / 10000));
console.log(`a ${sizeGib} GiB upload needs parts of at least ${partMib} MiB: ${Math.ceil((sizeGib * 1024) / partMib)} parts`);import java.util.HashMap;
import java.util.Map;
public class Main {
static final int TTL = 60; // seconds an edge or shield may serve a cached copy
static class EdgeCache {
final Map<String, Integer> expires = new HashMap<>();
boolean fresh(String obj, int now) { return expires.getOrDefault(obj, -1) > now; }
void store(String obj, int now) { expires.put(obj, now + TTL); }
}
// One request a second for 300 s, spread over the edges, for 10 objects.
static int[] run(int edges, boolean useShield) {
EdgeCache[] edgeCaches = new EdgeCache[edges];
for (int i = 0; i < edges; i++) edgeCaches[i] = new EdgeCache();
EdgeCache shield = new EdgeCache();
int edgeHits = 0, origin = 0;
for (int t = 0; t < 300; t++) {
String obj = "img" + (t * 7 % 10);
EdgeCache edge = edgeCaches[t % edges];
if (edge.fresh(obj, t)) { edgeHits++; continue; }
if (useShield && shield.fresh(obj, t)) { edge.store(obj, t); continue; } // filled from the shield, not the origin
origin++;
if (useShield) shield.store(obj, t);
edge.store(obj, t);
}
return new int[] {edgeHits, origin};
}
public static void main(String[] args) {
for (int edges : new int[] {3, 12}) {
int[] without = run(edges, false), with = run(edges, true);
System.out.printf("%d edges: edge hit ratio %d%%, origin fetches %d without a shield, %d with one%n",
edges, Math.round(without[0] / 3.0), without[1], with[1]);
}
// Multipart upload: S3 allows up to 10,000 parts, each 5 MiB to 5 GiB (the last part may be smaller).
int sizeGib = 200;
int partMib = Math.max(5, (int) Math.ceil(sizeGib * 1024 / 10000.0));
System.out.printf("a %d GiB upload needs parts of at least %d MiB: %d parts%n", sizeGib, partMib, (int) Math.ceil(sizeGib * 1024.0 / partMib));
}
}3 edges: edge hit ratio 50%, origin fetches 150 without a shield, 50 with one
12 edges: edge hit ratio 0%, origin fetches 300 without a shield, 50 with one
a 200 GiB upload needs parts of at least 21 MiB: 9753 parts
7. Say it in the interview
- "Where do the photos go?" The bytes go in object storage, uploaded directly by the client with a presigned URL, and the metadata goes in the database. Downloads go through a CDN with long TTLs and versioned URLs.
- "How do you handle a 20 GB upload?" Multipart upload: parts sent in parallel and retried individually, then completed in one call. Resumable after a dropped connection.
- "How do you update a cached file?" Publish it under a new versioned URL and update the reference; purge only for mistakes, because purges take time to reach every edge.
8. Common mistakes
9. Check yourself
A photo app's servers receive every upload and then write it to S3. What's the better design?
You add more CDN edge locations and the origin's load goes up. Why?
How should a changed logo reach users through a CDN?
When might Cloudflare R2 be cheaper than a big cloud's object store?
10. Sources and further reading
Product facts checked on 3 November 2026. Limits, classes and retirement dates change; check before relying on them.
| Source | Used for |
|---|---|
| Amazon S3 increases the maximum object size to 50 TB | Object size limit |
| Optimizing Amazon S3 performance | Request rates per prefix |
| Azure CDN Standard from Microsoft (classic) retirement FAQ | Retirement date and Front Door migration |
| Module 10: Blob storage and CDNs | Concepts this guide builds on |