S3 / Blob Storage
Effectively infinite, cheap, durable object storage for large unstructured files — images, video, backups, logs — that should never live in your database.
Also worth naming: Amazon S3 · Google Cloud Storage · Azure Blob Storage · Cloudflare R2 · MinIO (self-hosted, S3-compatible)
The rule is simple: large bytes go in object storage, a pointer goes in your database. Treat S3 as an infinitely scalable, eleven-nines-durable bucket of files you reach by key, fronted by a CDN for delivery.
What it is
Object storage is a service for storing large, unstructured blobs — images, videos, documents, backups, log archives, ML datasets — addressed by a key within a bucket. You PUT an object and get back a URL; you GET it by key. There are no rows, no schema, no queries — just durable, cheap, effectively unlimited file storage.
Its defining properties make it boring in the best way: it is effectively infinitely scalable (you never think about capacity), extremely durable (S3 advertises eleven nines — 99.999999999% — via replication and erasure coding across devices and facilities), and cheap (cents per GB-month, far less than storing the same bytes in a database). In an interview you can treat its scalability and durability as given and spend your time on how data flows in and out.
The canonical pattern is store the blob in S3, store a pointer (URL/key) in your database. The database indexes and queries the metadata with low latency; S3 holds the heavy bytes cheaply. Uploads go direct from the client via pre-signed URLs so your app never proxies the bytes, and downloads are served through a CDN with the bucket as origin. Reach for object storage whenever a system stores files bigger than a few KB — and never use it as a primary database or for low-latency random access to structured data.
When to reach for it
Reach for this when…
- You store large files — images, video, audio, PDFs, user uploads, ML data
- You need durable, cheap, effectively unlimited capacity for unstructured bytes
- Static assets or media to serve globally (paired with a CDN)
- Backups, log/event archives, data-lake storage, or a staging area for batch processing
Not really this pattern when…
- The data is structured and you query it by field (that is a database)
- You need low-latency random reads/writes or transactions (database / cache)
- You need to modify parts of a file in place — objects are written and replaced whole
- Tiny values where a database row or cache entry is simpler and faster
How it works
A few facts cover almost every interview use:
1. Objects, not files or rows. Each object is a blob plus metadata under a key in a flat namespace (the "folders" in a key like users/42/avatar.png are just a naming convention). Objects are written and read whole — there is no in-place edit and no partial append; to "change" an object you upload a new version. This is why it is great for media and terrible as a mutable database.
2. Durability and scale are givens; design around access, not capacity. Providers replicate and erasure-code across facilities for eleven-nines durability and present effectively unlimited capacity. So in a design you don't size S3 — you focus on the upload path, the download path, and lifecycle/cost.
3. Direct-to-blob uploads via pre-signed URLs. The client requests a short-lived signed URL scoped to one object; it then PUTs the bytes straight to S3, bypassing your app entirely. The app validates and records metadata but never proxies the file — which is what makes uploads scale.
The client asks the app for permission, gets a short-lived signed URL scoped to one object, and uploads straight to S3. The app stays out of the data path, so its bandwidth and memory are spent on metadata, not file bytes.
4. Serve through a CDN, with the bucket as origin. For downloads, put a CDN in front so objects are cached at the edge near users; the origin bucket only sees cache misses. Private content uses signed URLs / signed cookies with short expiry so only authorised users can fetch.
The bucket is the durable origin of record; a CDN caches objects at edge locations near users. Downloads are served from the edge with signed URLs for private content, so the origin handles only cache misses.
5. Large uploads use multipart. Files over ~100 MB are uploaded in parallel chunks (S3 multipart / the tus protocol) so a network blip retries one part instead of the whole file — and a lifecycle rule aborts abandoned multipart uploads so they don't accrue cost.
Performance envelope
Object storage characteristics — the numbers to quote.
| Dimension | Number | Why it matters |
|---|---|---|
| Durability | ~11 nines (99.999999999%) | Replication + erasure coding; treat as effectively never-lost |
| Scalability | Effectively unlimited | Never a capacity concern in a design — treat as given |
| Object size | Bytes to 5 TB (multipart above ~100 MB) | Holds anything; chunk the big ones |
| Throughput | ~3,500 PUT / 5,500 GET per sec per prefix | Spread keys across prefixes for very high throughput |
| Latency | Tens to ~100+ ms first byte | Not a low-latency store — front with a CDN/cache |
| Cost | ~$0.02/GB-month (cheaper cold tiers) | Far cheaper than DB storage; tier to cut it further |
Capabilities in interviews
Media & user-upload store
Hold images, video, and documents cheaply while the database keeps queryable metadata.
The universal pattern: bytes in S3, pointer in the DB.
S3: s3://media/posts/991/video.mp4
DB: posts(id=991, ..., media_url="s3://media/posts/991/video.mp4", status="ready")The database indexes and queries the metadata (owner, status, created_at) with low latency; S3 holds the heavy bytes for cents. Used by essentially every product that stores user media — YouTube videos, Instagram images, Dropbox files all follow this shape.
Choose this variant when
- User uploads of any kind
- Storing media referenced by a database row
- Anywhere a blob is bigger than a few KB
Direct-to-blob uploads
Pre-signed URLs (and multipart for big files) keep file bytes out of your app entirely.
The app issues a scoped, short-lived signed URL; the client uploads directly to S3:
POST /uploads → app validates size/type/auth → returns presigned PUT URL
client → PUT bytes → S3 → event → app marks the DB row "ready"For files over ~100 MB, the client uses multipart upload: split into chunks, upload in parallel, and a failed chunk retries alone. The app's bandwidth bill is metadata and events, not gigabytes of media — which is the only way uploads scale past toy load.
Choose this variant when
- Any non-trivial file upload
- Large or flaky-network uploads (multipart/resumable)
- Protecting app servers from proxying bytes
Static asset & media delivery
Use the bucket as a CDN origin to serve assets and downloads globally from the edge.
S3 is the durable origin; a CDN caches objects near users so downloads are fast and the origin sees only misses:
client → CDN edge (hit) → bytes
↳ (miss) → S3 origin → cache + serveFor public assets, this is plain caching. For private content (paid video, user files), issue signed URLs / signed cookies with short expiry so the CDN only serves authorised requests. This offloads ~95%+ of download traffic from the origin.
Choose this variant when
- Serving images / video / downloads globally
- Static website or front-end asset hosting
- Paid or private media via signed URLs
Backups, archives & data lake
Cheap durable storage for backups, log archives, and analytics datasets — with lifecycle tiering.
Object storage is the default landing zone for backups, exported logs/events, and data-lake files that batch jobs (Spark, Athena, a warehouse loader) read later:
events → S3 (parquet, partitioned by date) → Athena / Spark / warehouse loadLifecycle policies move data through cheaper tiers as it ages (standard → infrequent-access → archive/Glacier) and eventually delete it, so cold data costs a fraction of hot data. This is how you retain years of history affordably.
Choose this variant when
- Database / system backups
- Log and event archival
- Data-lake / analytics staging with lifecycle tiering
Operating knobs
Storage class / lifecycle tiering
Match the class to access frequency: Standard for hot data, Infrequent-Access for occasional reads, Glacier/Archive for cold backups (cheap to store, slow/costly to retrieve). A lifecycle policy transitions objects automatically as they age and expires them at end of life — the main lever for controlling storage cost over time.
Access control & signed URLs
Buckets are private by default. Serve private content with short-lived pre-signed URLs (or CDN signed URLs/cookies) scoped to one object, rather than making the bucket public. Bake content-type and size limits into upload URLs so the client cannot abuse them — the signed URL is a security boundary, not a convenience.
Key / prefix design for throughput
Request rate scales per prefix (~3,500 PUT / 5,500 GET per second each). For very high throughput, spread keys across many prefixes (e.g. a hash or shard prefix) rather than a single hot prefix. For most systems the default is plenty; for write-heavy ingest it matters.
Multipart & abandoned-upload cleanup
Use multipart upload for large files (resumable, parallel) and set a lifecycle rule to abort incomplete multipart uploads after N days — otherwise half-finished uploads silently accumulate storage and cost. Versioning (optional) protects against accidental overwrite/delete at extra storage cost.
Versus the alternatives
Object storage vs the alternatives.
| Dimension | S3 / Blob | Database (Postgres/Dynamo) | CDN |
|---|---|---|---|
| Stores | Large unstructured blobs | Structured rows / items | Cached copies of objects |
| Access | Whole-object GET/PUT by key | Query / index by field | Edge reads near users |
| Durability/scale | ~11 nines, unlimited | Durable, bounded by ops | Ephemeral cache |
| Cost | Cheapest per GB | Expensive per GB | Per request + egress |
| Role | Source of truth for files | Source of truth for metadata | Delivery layer over S3 |
Failure modes & gotchas
Streaming file bytes through your app server burns its bandwidth and memory and caps concurrency — ten simultaneous 500 MB uploads can pin gigabytes of RAM. Use pre-signed URLs for direct-to-S3 upload and a CDN for download; keep the app on the metadata path only.
Putting images or video as BLOB columns bloats the database, slows backups, and costs far more per GB than object storage. Store the bytes in S3 and a URL/key in the row — the database stays small and queryable.
Making a bucket public to "make it work" is a classic data-leak. Keep buckets private and serve via short-lived signed URLs scoped to one object, with content-type/size constraints on upload URLs so they cannot be abused.
If the origin returns an error or you replace an object, the CDN can serve a cached error or stale bytes until TTL. Use content-hashed keys (a new version = a new key, never purge) for immutable assets, and never cache error responses with a long TTL.
Incomplete multipart uploads keep their uploaded parts (and bill for them) until explicitly aborted. Always set a lifecycle rule to abort incomplete uploads after a few days, or storage silently leaks.
In production
Netflix
The entire catalog on S3, served from a custom CDN
Netflix runs almost entirely on AWS and uses S3 as the durable store of record for its media assets and data — hundreds of petabytes. Every title is ingested, transcoded into dozens of renditions (resolutions, codecs, audio tracks), and the encoded files live in S3 as the origin of truth. The classic pattern: heavy bytes in object storage, metadata in databases, processing triggered off the storage layer.
For delivery, Netflix built Open Connect, its own CDN of appliances inside ISPs, pulling from the S3 origin — the "S3 as origin, CDN in front" pattern at planetary scale. The lesson engineers cite: object storage is the cheap, durable, infinitely scalable foundation you build media on; you never stream from it directly to users, you front it with a cache/CDN.
Dropbox
Built on S3 — then outgrew it at exabyte scale
Dropbox is a two-sided lesson. For its first years it stored all user file data on Amazon S3 (with metadata in its own databases) — the textbook "pointer in the DB, bytes in object storage" architecture that let a small team scale to hundreds of millions of users without operating storage hardware.
Then, at exabyte scale, Dropbox famously migrated off S3 onto its own purpose-built object store, "Magic Pocket," because at their specific scale and access pattern, owning the hardware was dramatically cheaper. This is the senior nuance worth citing: object storage is the right default and scales astonishingly far, but it is a cost/control trade — at a few companies' scale, building your own S3-compatible store pays off. For 99.9% of systems, you use the managed service.
Good vs bad answer
Interviewer probe
“Design the storage for a video platform where users upload videos up to a few GB and others stream them worldwide.”
Weak answer
"Store the videos in the database as binary so they're safe and transactional, and the app streams them out to viewers when requested."
Strong answer
"Videos go in S3, never the database — the DB just holds metadata with a pointer: videos(id, owner, status, s3_key). Upload: the client requests a pre-signed multipart URL (videos are multi-GB, so multipart gives parallel chunks and resume on a dropped connection), uploads the bytes directly to S3 so our app never proxies gigabytes, and an S3 event triggers transcoding and flips the row to 'ready'. Delivery: S3 is the durable origin behind a CDN, so streams are served from edge locations near viewers and the origin only sees cache misses; paid content uses signed URLs with short expiry. S3 gives us eleven-nines durability and effectively unlimited capacity for cents per GB, with lifecycle rules to tier cold uploads cheaply. Putting video in the database would bloat it, wreck backups, cost an order of magnitude more, and force every byte through our app — the exact opposite of what scales."
Why it wins: Applies the bytes-in-S3/pointer-in-DB rule, uses multipart direct upload to keep the app off the data path, fronts delivery with a CDN + signed URLs, cites durability/cost/lifecycle, and explains precisely why the DB-blob approach fails.
Interview playbook
When it comes up
- Any system that stores files — images, video, documents, user uploads
- Static asset or media delivery at global scale
- Backups, log archives, or a data lake / analytics staging area
- The interviewer asks "where do the actual files live?"
Order of reveal
- 11. Bytes in S3, pointer in the DB. Large files go in object storage; the database keeps queryable metadata with a URL/key — never blobs in the DB.
- 22. Direct-to-blob upload. Clients upload straight to S3 via a pre-signed URL (multipart for big files), so the app stays off the data path.
- 33. Treat scale/durability as given. Eleven-nines durable and effectively unlimited — I spend my time on the upload, download, and lifecycle, not capacity.
- 44. Deliver via CDN. S3 is the origin behind a CDN; downloads serve from the edge, signed URLs gate private content.
- 55. Cost via lifecycle. Lifecycle rules tier cold data to cheaper classes and clean up abandoned multipart uploads.
Signature phrases
- “Large bytes in object storage, a pointer in the database.” — The one rule that drives every file-storage decision.
- “The app never touches the file bytes — pre-signed direct upload.” — Shows you know how uploads actually scale.
- “S3 is the durable origin; the CDN is the delivery layer.” — Correctly separates storage from delivery.
- “Durability and capacity are givens — design the data flow.” — Focuses interview time where it matters.
Likely follow-ups
?“How does a multi-GB upload work without killing your servers?”Reveal
The client requests a multipart pre-signed upload, splits the file into chunks (e.g. 100 MB), and uploads the parts in parallel directly to S3 — never through the app. A dropped connection retries only the failed part, not the whole file, and the client shows real progress. On completion, S3 fires an event that triggers post-processing (transcode, virus scan) and marks the metadata row ready. The app issues URLs and records metadata; its bandwidth and memory are never spent on file bytes, which is the only way this scales to many concurrent large uploads. A lifecycle rule aborts any abandoned multipart uploads so they do not accrue cost.
?“How do you serve private files — say paid videos — securely and fast?”Reveal
Keep the bucket private and serve through a CDN using signed URLs or signed cookies with a short expiry, scoped to the specific object and user. The client gets a time-boxed URL, the CDN validates the signature and serves from the edge (fast, global), and the origin bucket is never publicly readable. I only stream bytes through the app when I need per-request logic the CDN cannot do — like DRM license issuance — and even then only the license, not the media payload.
?“When would object storage be the wrong choice?”Reveal
When the data is structured and you query it by field, when you need low-latency random access or transactions, or when you need to modify parts of a file in place — objects are written and replaced whole, not edited. Those are database or cache jobs. Object storage is also overkill for tiny values where a row or a cache entry is simpler. It is specifically for large, unstructured, write-once-read-many blobs.
Worked example
Setup. Design storage and delivery for a video platform: users upload clips up to a few GB; millions of viewers stream them worldwide; the catalog grows without bound.
The move. Videos go in S3, never the database — the DB holds metadata with a pointer: videos(id, owner, status, s3_key, duration). Upload uses a pre-signed multipart URL: because clips are multi-GB, the client splits the file into ~100 MB parts and uploads them in parallel, directly to S3, so the app never proxies a single byte; a dropped connection retries one part, not the whole file. On completion an S3 event triggers transcoding (into HLS renditions) and flips the row to ready.
Delivery. S3 is the durable origin behind a CDN, so streams are served from edge locations near each viewer and the origin only sees cache misses; paid content uses signed URLs with short expiry so only entitled users can fetch.
Cost + durability. S3 gives ~11 nines of durability and effectively unlimited capacity for ~cents/GB — I treat both as given and instead design the data flow. Lifecycle rules tier cold uploads (Standard → Infrequent-Access → Glacier) and abort abandoned multipart uploads so half-finished uploads don't silently accrue cost.
What breaks. A naive design proxies bytes through the app — ten concurrent 500 MB uploads would pin gigabytes of app RAM; the signed-URL direct path removes that entirely. The other trap is a public bucket; I keep buckets private and bake content-type + size limits into the upload URL so it can't be abused.
The result. Multi-GB uploads that never touch the app servers, global low-latency streaming from the edge, eleven-nines durability at cents/GB, and a database that stays small and queryable — the storage shape behind essentially every media product.
Cheat sheet
- •Large unstructured bytes → object storage. Pointer (URL/key) → database. Never blobs in the DB.
- •Durability ~11 nines, effectively unlimited scale — treat both as given; design the data flow.
- •Uploads go direct to S3 via short-lived pre-signed URLs; the app never proxies bytes.
- •Files over ~100 MB → multipart (parallel, resumable). Abort abandoned uploads via lifecycle.
- •Serve downloads via a CDN with S3 as origin; signed URLs/cookies gate private content.
- •Objects are whole-write, no in-place edit — replace to change; great for media, bad as a DB.
- •Lifecycle tiering (Standard → IA → Glacier → delete) is the main cost lever.
- •Keep buckets private; bake content-type + size limits into upload URLs.
Drills
Why store a video in S3 and only a URL in the database instead of the video itself?Reveal
Because databases are built for small, structured, queryable rows with low-latency indexed access, and object storage is built for large, cheap, durable bytes. A multi-GB BLOB column bloats the database, makes backups slow and huge, costs roughly an order of magnitude more per GB, and forces every byte through the database connection. Keeping the bytes in S3 and a pointer in the row keeps the database small and fast for the queries it is good at, while S3 handles the heavy storage and a CDN handles delivery.
Interviewer: "10,000 users upload simultaneously. Walk me through it without overloading the app."Reveal
Each client POSTs metadata; the app validates size/type/auth and returns a pre-signed (multipart, for large files) upload URL, then is done — it does not receive the bytes. All 10,000 clients PUT directly to S3 in parallel; S3 is effectively infinitely scalable, so concurrency is its problem, not the app's. On each completion S3 fires an event to a queue, and a worker pool post-processes (transcode/scan) and flips the DB row to ready. The app's load is 10,000 small metadata requests and 10,000 events — trivial — instead of terabytes of file bytes.
Your CDN is serving a stale image after you replaced it. How do you avoid this?Reveal
Use content-hashed keys for immutable assets: when the image changes, write a new object under a new key (e.g. avatar.<hash>.png) and update the pointer, so the new URL is a guaranteed cache miss and the old one simply ages out — no purge needed. If you must reuse the same key, explicitly invalidate/purge it at the CDN after replacing, accepting propagation delay. Either way, set sensible cache-control, and never cache error responses with a long TTL.
What it is