Learn how CDNs cache content near users, how object storage holds files durably, and how presigned URLs and multipart uploads power a photo upload flow.
What you'll learn: By the end of this lesson you will be able to explain how a CDN serves content from edge locations, set cache headers and cache keys sensibly, describe object storage concepts such as buckets, durability and presigned URLs, and design the upload and download flow for images and video.
A national bookshop chain does not ship every book from one warehouse in one city. It keeps popular titles in stores near customers, and restocks those stores from the central warehouse only when something is missing. The warehouse holds everything and is the safe long-term home. The local stores make the common case fast.
That is the pairing in this lesson. Object storage is the warehouse: a huge, durable, cheap place to keep files. A CDN (content delivery network) is the network of local stores: servers near users that keep copies of popular files so most requests never travel far or reach your own servers.
The running example is a real-estate listing site. Every listing carries dozens of large photos, 360-degree panoramas and walkthrough videos. Bytes dominate the bill and the bandwidth. If application servers had to read every image from disk and push it to every viewer, they would drown.
The core idea
Separate three kinds of work and give each a specialist.
Work
Specialist
Why
Business logic: who can see which listing, search, enquiries
Application servers and database
Needs code and transactions
Storing large files durably
Object storage
Built for huge numbers of big files, cheap, highly durable
Delivering files quickly to users worldwide
CDN
Copies near users, absorbs traffic spikes
The application decides who may upload or view a file and records metadata in the database. The bytes themselves flow directly between users, object storage and the CDN, mostly bypassing the application. This keeps the app tier small and cheap.
How a CDN works
A CDN is a geographically distributed set of caching servers called edge locations or points of presence (PoPs). Your own servers, where the original content lives, are the origin.
The user's browser asks for https://cdn.example.com/photos/abc.jpg. DNS (the system that turns a name into a server address) directs the browser to a nearby edge, using techniques such as geographic DNS or anycast (see load balancing).
If the edge already has a fresh copy (a cache hit), it returns it immediately.
If not (a cache miss), the edge fetches the file from the origin, stores a copy according to caching rules, and returns it.
The next nearby user gets the cached copy.
sequenceDiagram
participant U as User
participant E as CDN edge
participant O as Origin
U->>E: GET photo abc.jpg
E->>O: miss, fetch abc.jpg
O-->>E: file with cache headers
E-->>U: file
U->>E: another user GET abc.jpg
E-->>U: served from edge cache
Only the first request pays the trip to the origin; everyone after it, until the copy expires, is served from the edge.
Why does this help? Distance. A request across an ocean costs on the order of a hundred milliseconds or more in round-trip time, while a nearby edge may respond in a few milliseconds to tens of milliseconds (rough approximations). A CDN also shields the origin: a thousand people requesting one popular photo can cause as little as one origin fetch per edge, since many CDNs collapse simultaneous misses into one fetch.
Model
How content arrives
Good for
Cost or risk
Pull (origin pull)
Edge fetches from the origin on the first request
Most websites; large or unpredictable catalogues
First request in each region is slower; origin handles misses
Push
You upload content to the CDN in advance
Predictable large launches such as a software release
You manage uploads and storage on the CDN and must delete old content yourself
Pull is the default, with an object storage bucket as the origin. Some CDNs also offer an origin shield or mid-tier cache that collects misses from many edges into one request to the origin.
Cache headers, TTL and cache keys
A CDN has to know how long a file may be reused. The origin tells it through HTTP cache headers.
Cache-Control: max-age=31536000 allows caching for that many seconds (one year). This lifetime is the TTL (time to live).
public allows shared caches such as a CDN to store it; private restricts it to the user's browser; no-store says do not keep it. s-maxage sets a TTL specifically for shared caches.
ETag and Last-Modified let a cache ask "has this changed?" and get a cheap "not modified" answer instead of the full file.
immutable (a Cache-Control extension that some browsers honour by skipping revalidation) says the file will never change during its lifetime.
CDNs also let you set rules on the CDN side that override or supplement the headers. Defaults differ by provider, so check the documentation.
flowchart TD
F["A file to serve"] --> Q1{"Does the URL change when content changes?"}
Q1 -->|yes| A["public, max-age one year, immutable"]
Q1 -->|no| Q2{"Is it private or personalised?"}
Q2 -->|yes| B["private or no-store, or signed access"]
Q2 -->|no| C["Short TTL plus ETag revalidation"]
Start with the first question. Versioned URLs let you cache for a year; anything at a fixed address needs a short TTL or revalidation, and private content must never sit in a shared cache under a shared key.
Versioned URLs beat invalidation
When you change a file, cached copies around the world are stale. You have two strategies:
Invalidation (purge). Ask the CDN to delete cached copies of a path. It works, but it takes time to propagate, may be limited or charged by volume, and browsers may still hold their own copies.
Versioned or content-addressed URLs. Put a version or hash in the file name: /photos/abc_v2.jpg or /static/app.3f9c2e.js. When content changes, the URL changes, so there is nothing to purge.
For immutable uploads, such as a photo that is never edited, the object key can itself be unique and permanent.
Cache keys
A cache key is what the CDN uses to decide whether two requests are "the same". By default it is usually the URL, often including the query string.
Tracking or session parameters (?utm_source=..., ?session=...) can make every request a separate entry and wreck the hit ratio. Configure the CDN to ignore irrelevant parameters.
If the response differs by a header (for example Accept-Encoding or an image format preference), the key must include it or the response must use Vary correctly. Otherwise a user can get the wrong variant.
Never cache personalised responses under a shared key.
Object storage concepts
Object storage stores data as objects inside containers called buckets. An object is the file's bytes, a key (its name, such as listings/8841/photos/abc.jpg), and metadata (content type, custom tags). There are no real folders; slashes in keys are just part of the name, even though tools display them as folders. Examples include Amazon S3, Google Cloud Storage and Azure Blob Storage, plus self-hosted systems that speak S3-compatible APIs.
What makes it different from a disk or a database:
Whole-object operations. You put, get, delete or overwrite entire objects over HTTP. To change an object, you write a new version.
Huge scale and low cost per gigabyte. Designed for billions of objects.
Durability by redundancy. Providers typically store data redundantly across multiple devices and facilities and publish a durability figure. Do not confuse durability (will my data still exist?) with availability (can I read it right now?). Durability does not protect you from your own accidental delete unless you enable versioning or backups.
Consistency semantics vary by provider and over time. Rely on current documentation rather than old folklore.
Not a database. There is no rich querying. Keep searchable metadata in a database and use the object key as the link.
Need
Object storage
Block or file storage
Database
Millions of large immutable files
Excellent
Poor to fair
Poor
Fast random small reads and writes
Poor
Good
Good
Query by content or attributes
None (use an index elsewhere)
None
Good
Cost per terabyte
Low
Medium
High
Typical use
Images, video, backups, logs
Virtual machine disks, shared file systems
Structured records
Presigned URLs
How does a user upload a photo to a private bucket without giving them your credentials, and without pushing all the bytes through your servers? With a presigned URL: a temporary URL, created by your server with its credentials, that grants permission to perform one specific operation on one specific object until it expires. The app checks that the user is allowed to upload, generates a presigned PUT (or POST) URL valid for a short time such as ten minutes, and hands it back. The client then sends the file directly to object storage. Keep the expiry short, scope the URL to one key, and constrain content type and size where the provider allows it.
Multipart upload
Large files are uploaded in parts. In a multipart upload, the file is split into chunks (several megabytes each), uploaded in parallel, and then assembled by the storage service. If one part fails, only that part is retried; parts upload in parallel; and single-request size limits are avoided (limits differ by provider). Also arrange cleanup: abandoned multipart uploads keep occupying storage until aborted, so use a lifecycle rule (an automatic storage rule such as "abort incomplete uploads after seven days").
The upload and download flow
sequenceDiagram
participant C as Client
participant A as App server
participant S as Object storage
participant Q as Queue
participant W as Worker
participant D as Database
participant N as CDN
C->>A: request upload slot
A->>D: create photo row as pending
A-->>C: presigned URL and photo id
C->>S: upload original bytes
S-->>Q: object created event
Q->>W: process photo
W->>S: write resized variants
W->>D: mark photo ready
C->>N: view photo through CDN
N->>S: fetch on miss
N-->>C: image
The app tier handles only small JSON messages; the large bytes move straight between client, storage and CDN. In words: the app authenticates the user and creates a pending row with a unique object key; the client uploads straight to storage; storage emits an event that enqueues a job (message queues); a worker validates the file, strips sensitive metadata where appropriate, writes several sizes back to storage, and marks the row ready; pages then reference CDN URLs, not the bucket.
stateDiagram-v2
[*] --> Pending: slot requested
Pending --> Uploaded: object created event
Pending --> Expired: no upload before expiry
Uploaded --> Processing: worker picks up job
Processing --> Ready: variants written
Processing --> Failed: invalid or corrupt file
Failed --> Processing: retry
The row's status is the contract: the site shows only Ready photos, and background jobs clean up Expired ones.
Why keep the original and generate derivatives? Different screens need different sizes. Serving a 12-megapixel original to a phone showing a 400-pixel thumbnail wastes bandwidth and money. Pre-sized variants (or an image service that resizes on demand and caches the result) fix that.
IMPORTANT
Treat uploaded files as untrusted input. Validate file type by content, not just extension, enforce size limits, process in an isolated worker, and never serve user uploads from the same domain as your application's cookies, so a malicious file cannot run as part of your site.
Serving images, video and private content
Images. Use efficient formats where browser support allows, serve appropriately sized variants, set long immutable lifetimes on versioned URLs, and lazy load images below the fold.
Video. Do not serve a giant single file for streaming. The video is transcoded into several quality levels and split into short segments, and the player switches quality as the network changes (adaptive bitrate streaming; HLS and DASH are common formats). Segments are ordinary static files, ideal for CDN caching. The full design is in the photo sharing case study and the video streaming case study later in the course.
Private content. Public CDN URLs can be shared by anyone who has them. For restricted content use signed URLs or signed cookies supported by your CDN, with expiry times. Check how your CDN builds the cache key: if each signed URL counts as a separate key, caching is less effective, which is one reason signed cookies suit assets made of many files.
Cost levers
Media systems often spend more on bandwidth and storage than on compute. Pricing models vary, so check your provider.
Cost driver
What it is
How to reduce it
Egress (data transfer out)
Charges for bytes leaving the cloud or CDN to the internet
Raise CDN hit ratio, compress, resize, serve from the edge
Storage
Per gigabyte per month
Delete unneeded derivatives, use lifecycle rules
Storage tiers
Hot, infrequent access and archive classes
Move old, rarely viewed originals to colder tiers; remember retrieval fees and delays
Requests
Per-request charges on storage and CDN
Cache aggressively; avoid needlessly many tiny objects
Origin fetches
Misses that hit storage or compute
Origin shield, longer TTLs, versioned URLs
In practice: photos and virtual tours for a real-estate listing site
NOTE
A realistic scenario, not a case history. This is a composite example built from common production patterns. It does not describe any specific company, and every number in it is an illustrative assumption.
The situation
A real-estate listing site shows about 300,000 active listings, each with about 30 photos, a 360-degree panorama set and sometimes a short walkthrough video. Assume 9 million photos averaging 6 MB as originals, roughly 54 TB. A Node.js and TypeScript API runs on Kubernetes with PostgreSQL for listings; originals sit in an S3-compatible bucket behind a CDN; Prometheus and Grafana provide metrics and Terraform manages infrastructure. A team of six engineers supports the web and mobile apps, and agents upload from phones on cellular networks.
The pain, with illustrative numbers: the listing page transfers about 14 MB of photos because it serves originals. Uploads of 40 MB batches go through the API, which times out on slow connections. The CDN hit ratio is about 55 percent because image URLs carry session and tracking query strings. The monthly egress line is the largest item on the cloud bill, and photos of sold listings stay in the hot tier forever.
Starting architecture
Uploads consume API capacity, and the browser downloads originals through a CDN whose cache keys are fragmented.
Options considered
Option
What it fixes
What it costs
Risk
Do nothing smaller: raise timeouts, compress originals on upload
Some failed uploads
Little; client-side work
Page weight and egress stay high
Presigned direct uploads, worker-generated variants at three widths, versioned immutable URLs, normalised cache keys, lifecycle tiering for sold listings
Upload failures, page weight, hit ratio, storage cost
A worker fleet, a queue, a backfill job
Medium; backfill of existing photos is slow and must be throttled
On-demand resizing at the edge or through an image service
Any size on request, no variant storage
Per-transformation cost, another dependency
Medium; unbounded variants can lower hit ratio
The decision
Field
Decision record
Context
Large originals are served and uploaded through the wrong paths, and cache keys are polluted.
Decision
Direct multipart uploads with presigned URLs; a worker creates 400, 800 and 1600 pixel variants; URLs include a content hash; the CDN ignores tracking parameters; virtual tours use signed cookies; sold listings move to a colder storage class after a period set by the business.
Consequences, good
App tier no longer handles bytes; smaller pages; hit ratio can exceed 90 percent; cost falls with tiering.
Consequences, bad
More moving parts; a status model for photos; a backfill to run; colder tiers add retrieval fees if an old listing is relisted.
Revisit when
Designers need more than four image sizes, or the variant storage cost exceeds the on-demand transformation cost for a month.
The measurable targets: first-view image transfer under about 1.5 MB, CDN hit ratio above 90 percent, and upload success rate above 99 percent on mobile.
Rollout and verification
Listing site target architecture: presigned uploads, queue, resize workers, variants behind the CDNAgent app to Bucket (multipart); Bucket to Queue (event); Queue to Resize workers; Resize workers to Bucket (read original); Resize workers to Bucket (write variants); Resize workers to PostgreSQL; Buyer browser to CDN; CDN to BucketAgent app: presigned URLAgent apppresigned URLBucket: originalsBucketoriginalsQueueQueueResize workersResize workersPostgreSQL: photo statusPostgreSQLphoto statusBucket: variantsBucketvariantsBuyer browserBuyer browserCDN: normalised keyCDNnormalised keymultiparteventread originalwrite variants
Roll out in stages: new uploads first, behind a flag for a subset of agents, while old listings keep the old path. Then run a throttled backfill that creates variants for existing photos, newest listings first, rate limited so it cannot saturate the workers or storage requests. Switch the listing page to variants for a small share of traffic and compare. Dashboards: CDN hit ratio and origin requests per second, bytes per page (from real-user monitoring), upload success rate and part retry count, queue depth and oldest message age, worker failure rate, and a daily egress cost graph with a budget alert.
The 2 a.m. failure
A web deploy at 01:40 adds a cache-busting query parameter to every image URL. At 02:10 the page says "CDN hit ratio below 60 percent, origin request rate high".
flowchart TD
D["Deploy adds ?cb=timestamp to image URLs"] --> K["Every request is a new cache key"]
K --> M["CDN misses on almost every image"]
M --> O["Origin requests and egress spike"]
O --> T["Storage throttling or cost alert fires"]
Detection: the hit-ratio panel drops while origin requests per second jump and an egress budget alert fires. The first three checks: fetch one image URL twice with curl -s -o /dev/null -D - "<url>" and look at the Age and cache-status headers (the repeat should be a hit), then change only the cb value and confirm it becomes a miss, which proves the parameter is part of the cache key; list top requested URLs in the CDN logs grouped by path and count of distinct query strings; and review what changed in the deploy. Mitigation: add the parameter to the CDN's ignored-parameters list (or roll back the deploy), then watch hit ratio recover. Follow-up: keep cache-key rules in Terraform, add a CI check that image URLs are stable across builds, and alert when hit ratio stays under a set threshold for ten minutes.
Architect's notes
Measure bytes per page before designing variants. Use real-user monitoring to see what mobile users actually download. Variants are a means; page weight is the goal.
Ask for the retention policy. Who decides how long sold-listing photos stay, and who pays for retrieval from colder tiers? Without an answer, lifecycle rules become a late argument.
Egress is a product decision. Autoplaying walkthrough videos cost real money. Ask product whether the default can be a poster image with play on tap.
Backfills are projects. Estimate the number of objects and per-request costs before starting, and make the job resumable and rate limited.
Decide who is paged. Hit ratio and origin errors sit between the platform team (CDN, bucket) and the application team (URLs). Agree ownership and a single alert path.
Defer on-demand transformation. Fixed variants are simple to cache and reason about. Revisit only when the design genuinely needs arbitrary crops.
Common mistakes
Serving files through the application servers. Large downloads tie up app capacity. Use presigned URLs for uploads and a CDN for downloads.
Short TTLs on immutable files. You throw away the main benefit. Use unique URLs and long TTLs.
Relying on purges for correctness. Invalidation is slow and limited. Prefer versioned URLs.
Polluted cache keys. Random query parameters or unneeded headers fragment the cache. Normalise keys.
Caching personalised responses under a shared key. It leaks data between users. Vary correctly or do not cache.
Public buckets by accident. A misconfigured bucket exposes data. Keep buckets private, serve through the CDN with controlled access, and review permissions.
Presigned URLs with long expiry and broad scope. A leaked URL becomes a standing credential. Keep them short-lived and specific.
Forgetting lifecycle rules. Abandoned multipart uploads, temporary files and old derivatives pile up silently.
Key takeaways
Bytes should skip your app servers. Use object storage for files and a CDN for delivery.
A CDN moves copies close to users. It lowers latency, absorbs spikes and shields the origin.
Pull is the usual model. The edge fetches from the origin on a miss; push suits predictable large releases.
Version your URLs. Content-addressed or versioned names allow long TTLs and avoid purges.
Mind the cache key. Normalise query strings and use Vary correctly so the hit ratio stays high and responses stay correct.
Object storage is durable and cheap, not a database. Store bytes there and metadata in a database, linked by the object key.
Presigned URLs and multipart uploads scale uploads. Clients upload directly, with short-lived scoped permission and resumable parts.
Watch egress and tiers. Hit ratio, image sizing and lifecycle rules control the bill.
Practice exercises
Exercise 1 — Starter (30 min): For each of these files, write the Cache-Control header you would send and why: a logo at a fixed URL, a JavaScript bundle named with a content hash, an agent's profile photo at /agents/42/avatar, a private contract PDF, and a listing photo with a unique permanent key.
Exercise 2 — Intermediate (2 hours): Estimate the origin traffic for a listing site with assumed numbers: 1 million listing views per day, an average of 10 photos each, an average variant size of 150 KB, and CDN hit ratios of 55% and 92%. Treating every miss as one full origin fetch, calculate how many bytes per day reach the origin in each case, and how much origin traffic the better hit ratio saves. State your assumptions and show the arithmetic.
Exercise 3 — Advanced (half day): Using any S3-compatible service or a local emulator, build the upload flow: an API that returns a presigned URL, a client script that uploads a file directly, and a worker that creates a resized variant when the object appears. Add a lifecycle rule that aborts incomplete multipart uploads. Then write a failure analysis: what state is the database in if the client uploads but the worker crashes, and how does your design recover?
Lesson 10 of 12 · System Design Foundations: From Zero to Your First Design
Learn how CDNs cache content near users, how object storage holds files durably, and how presigned URLs and multipart uploads power a photo upload flow.
In the rest of this lesson
Object storage concepts
The upload and download flow
Serving images, video and private content
Cost levers
In practice: photos and virtual tours for a real-estate listing site
Architect's notes
Enrolling is free. It unlocks every lesson in System Design Foundations: From Zero to Your First Design, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.
Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.
Get new articles by email
Free. We email a confirmation link first, and you can unsubscribe from any email.