Files, media and blob storage
The database stores who uploaded the video; something else entirely stores the video.
The database stores who uploaded the video, when, and for whom; something else entirely stores the video. Mixing those two jobs is the original sin of file handling — gigabytes of binary in a system engineered for transactional rows — and this chapter is the standard architecture that keeps them apart.
Object storage — what the contract actually says
S3-style stores speak a deliberately tiny language: PUT a blob at a key, GET it, DELETE it, list by prefix. No partial updates, no transactions across objects, no filesystem semantics — and in exchange: eleven-nines durability (many copies across zones), effectively unbounded scale, and pennies per gigabyte. Why filesystems lose at this game is the Haystack paper in this chapter’s reading: at billions of small files, filesystem metadata (directories, permissions, per-file inodes) costs more I/O than the photos — Facebook’s fix was to pack millions of images into huge append-only files with one in-memory index, turning any photo fetch into one seek. That is object storage’s soul: blobs as immutable values, metadata somewhere built for metadata.
Uploads — never proxy the bytes
The rookie topology streams uploads through your app servers into storage — burning your compute, your memory (Little’s Law with 100 MB requests is brutal), and your patience. The standard pattern inverts it with presigned URLs: the client asks your API “I want to upload photo.jpg”; the API authorises, writes a pending-media row, and returns a short-lived signed URL; the client PUTs directly to storage; a storage event (or client confirm) flips the row to ready. Your servers touch metadata only. Large files add multipart (upload in parallel chunks, retry only the chunk that failed — at-least-once, applied to bytes) and resumability for free.
The media pipeline
The uploaded original is almost never what you serve — video needs transcoding to multiple resolutions, images need thumbnails and format variants. This is the jobs chapter wearing headphones: upload-complete emits an event; workers consume, process, write derivatives back to storage under versioned keys; the metadata row tracks state (uploaded → processing → ready) and the product shows honest intermediate UI. Everything idempotent — the transcoder will run twice — and derivatives are derived data (rebuildable from the original), so they get lifecycle rules, not backups.
Serving, and the bill
Serving is the CDN chapter applied: immutable derivative keys cached hard at the edge; signed URLs for private content so authorisation stays yours while bytes stay on the CDN; range requests honoured for video seeking. The bill has two lines people forget: egress (bytes out cost multiples of bytes stored — the CDN is also a cost optimisation) and storage classes (hot originals → infrequent-access → archive, on lifecycle rules — old originals are cold by month two, and archive tiers charge for retrieval, so the class is a promise about access patterns, not just a discount).
Where it bites
- Blobs in the database — backups balloon, replication crawls, and the buffer pool evicts your actual rows.
- Orphans: storage writes that never got their metadata row (or vice versa) — the outbox discipline applies to files too; reconcile by listing.
- Serving private media by copying through the app tier “because auth” — signed URLs exist precisely for this.
- Unversioned derivative keys — a re-transcode now needs a CDN purge instead of a new name.