Video streaming, where the bytes never touch your service
A video platform is two systems that share a database: a pipeline that turns one uploaded file into dozens of small ones, and a delivery path in which your servers hand out URLs and the CDN hands out bytes.
The brief
Creators upload videos. Viewers watch them on phones, laptops and televisions, over connections that range from a good fibre line to a bus with two bars of signal.
A video must start playing within a couple of seconds, keep playing as the connection changes, and cost less to serve than it earns.
Requirements
Functional
- Upload a video of up to several gigabytes and process it into playable form
- Play a video from any point, adapting quality to the viewer's bandwidth
- Show the creator when processing is done and the viewer a catalogue and a watch page
- Count views, and resume where a viewer left off
Non-functional
- Time to first frame under two seconds on a typical connection
- Playback must not stall when bandwidth drops; it may lower quality
- Processing may take minutes; it must never lose an upload
- Bandwidth is the dominant cost and the design must keep it off the origin
Back-of-envelope
Assume
- 10,000 uploads a day, averaging 500 MB and 8 minutes
- 2 million viewing sessions a day, averaging 6 minutes watched
- Five renditions per video (240p to 1080p), totalling about 1.2× the source in bytes
- The CDN serves 95% of segment requests from its edge caches
- A viewer's player asks for a 4-second segment every 4 seconds
Therefore
- Upload ingest: 10,000 × 500 MB = 5 TB a day into object storage, about 60 MB/s averaged, spiky in practice. Through a presigned URL, none of it passes through an application server.
- Transcoding: 10,000 videos × 8 minutes × 5 renditions is 400,000 output-minutes a day. At roughly real-time per rendition on one core, that is about 280 core-days of work per day — a fleet of ~12 machines with 24 cores running flat out, or a managed transcoding service billed per minute.
- Delivery: 2 million sessions × 6 minutes at an average 3 Mbit/s is 2M × 360 s × 0.375 MB/s ≈ 270 TB a day. At 95% edge hit rate the origin (object storage) serves ≈13.5 TB a day; the CDN serves the rest. Egress from the origin is the cost line to watch.
- Segment requests: 2M sessions × 90 segments each = 180 million requests a day, about 2,100 a second on average. Every one of them is a static file with a stable URL, which is what makes a CDN able to take them.
- Manifest and metadata reads hit your service: one manifest per session and a handful of API calls, about 25 requests a second averaged. The service is small; the pipeline and the CDN are where the scale lives.
Change the edge hit rate from 95% to 80% and origin egress quadruples. The hit rate is a function of catalogue shape — a few popular videos cache well, a long tail does not — and it is the number that decides the bill more than any code you write.
The interface
What is stored
video_id · owner_id · title · status (UPLOADING | PROCESSING | READY | FAILED) · duration_s · source_key · created_at · versionStatus is the state machine the pipeline drives, and the conditional update PROCESSING → READY on the version column is what stops a retried transcoding job from overwriting a newer result.
video_id · quality (240p … 1080p) · codec · bitrate_kbps · manifest_key · segment_count · ready_atOne row per rendition, so the master manifest is built from what exists: a video can be READY at 480p while 1080p is still transcoding, and the player never asks for a rendition that is not there.
uploads/{videoId}/source.mp4 · streams/{videoId}/{quality}/{n}.ts · streams/{videoId}/master.m3u8 · thumbnails/{videoId}/{n}.jpgImmutable, content-addressed layout: a segment's URL never changes once written, which is what lets the CDN cache it for a year. A re-encode writes new keys under a new rendition id rather than overwriting.
(user_id, video_id) → position_s · updated_at; TTL 90 daysA key-value shape with one write per 15 seconds per viewer and one read per session start. It does not belong in the relational database that holds the catalogue, and it can be lost without losing anything a user would call data.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- A 3 GB upload fails at 90%. What does the client resend, and what does your service have to do about it?
- Trace a segment request from a player in a city you have no servers in. Which components does it touch, and which of them are yours?
- Why is a segment's URL immutable, and what happens to viewers when a rendition must be re-encoded?
- The transcoding queue is 8 hours deep. What do you change first, and what does the creator see meanwhile?
- View counts on the watch page disagree with the CDN's request counts by 40%. Which is right, and why?