Design YouTube
Upload, transcode, and stream video to a billion viewers: a processing pipeline on one side, a CDN problem on the other.
Last updated 2026-09-21. Difficulty: hard. Patterns: transcoding, cdn, adaptive-bitrate, pipelines, storage. Reported at Google and 5 more with Pro.
The interviewer asks, the candidate answers and draws, and you press Next. Pause to answer yourself at the key decisions, and ask the coach anything along the way.
Functional requirements
- Upload a video. Files up to several GB, resumable from a browser or phone, with a title, description and visibility. The video is watchable within minutes of the upload finishing.
- Watch a video. Playback starts in under two seconds anywhere in the world and adapts to the viewer's bandwidth without stalling. Seeking is instant.
- Video metadata and listing. Title, description, channel, duration, thumbnails, view count. Channel pages and a home page list videos; search is a separate system we only integrate with.
- View counting. Approximate counts on the page, accurate counts for creator analytics and payouts. Counts must resist simple replay inflation.
- Comments and likes. Modelled as a simple append and counter problem; we design the data path, not moderation.
- Visibility and takedown. Private, unlisted, public. A takedown must stop playback everywhere within minutes.
- Out of scope. Live streaming (mention how the pipeline changes), recommendations and ranking, search relevance, DRM licensing, monetisation and ads.
Non-functional requirements
- Scale (1 B DAU · 5 B views/day · 500 h uploaded/min). Reads outnumber writes by orders of magnitude and each read is megabytes per second. Bandwidth, not requests, is the cost.
- Start-up latency (< 2 s to first frame). Requires the first segments to be at the edge and the manifest tiny. This drives the CDN and segment-size design.
- Smooth playback (rebuffer ratio < 0.5 %). Adaptive bitrate must step down before the buffer empties. The hardest tradeoff: quality against stalls on variable networks.
- Upload to available (< 10 min for a 10-minute video). Transcoding is parallelised by chunk so wall-clock time does not scale with duration.
- Durability (no lost uploads). The original is the only copy the creator may have. It is written to replicated object storage before any acknowledgement.
- Availability (99.99 % for playback). Playback degrades gracefully: if metadata services are down, cached manifests still play.
- Cost (egress and storage dominate). Every design decision is checked against bytes stored per video and bytes shipped per view.
Back-of-envelope estimates
- Views per second: ~60 k avg · ~200 k peak. 5 B views/day ÷ 86 400 s ≈ ~58 k/s. Peak 3× in evening hours per region. Each view is a manifest fetch plus tens to hundreds of segment fetches from the CDN.
- Egress bandwidth: ~150 Tbit/s. Assume an average bitrate of 3 Mbit/s across quality levels and 50 M concurrent viewers at peak: 50 M × 3 Mbit/s = 150 Tbit/s. This is why the CDN is the design, not a box on it.
- Upload volume per day: ~720 k hours · ~2 PB raw. 500 h/min × 1 440 min = 720 000 h/day. Raw uploads average ~3 GB per hour of video (phone 1080p), so ~2 PB/day of originals arrive.
- Transcoded storage per day: ~1.5 PB. Each video becomes ~6 renditions (144p to 4K). Total output is roughly 0.7× the original size after modern codecs: 2 PB × 0.7 ≈ 1.5 PB/day, ~550 PB/year plus originals kept cold. Storage tiering is mandatory.
- Transcoding compute: ~60 k cores continuously. Transcoding 1 h of video into all renditions takes roughly 2 core-hours of a modern CPU (hardware encoders are faster). 720 k h/day × 2 = 1.44 M core-hours/day ÷ 24 ≈ 60 k cores busy all day, more at peak upload hours.
- Segments per view: ~150. A 10-minute video in 4-second segments is 150 segments per rendition. A viewer fetches ~150 segment files plus a manifest and, on quality switches, a few extra. At 60 k views/s that is ~9 M segment requests/s at the edge.
- View events per day: 5 B · ~1 TB. One view event of ~200 B per view: 5 B × 200 B = 1 TB/day of events, plus heartbeat events for watch time (every 30 s while playing), roughly 10× that. Fine for a streaming pipeline; not fine for a row-per-event database.
Components
- Player / uploader: The web or mobile app. Uploads in resumable chunks directly to object storage using pre-signed URLs. Plays video with an adaptive-bitrate player that reads a manifest and fetches segments from the CDN, switching quality based on measured throughput and buffer level.
- CDN (multi-tier edge cache): Serves manifests, segments and thumbnails from PoPs near viewers. Multi-tier: edge → regional shield → origin, so a popular video is fetched from origin once per region. Signed URLs enforce visibility; short TTLs on manifests make takedowns fast.
- API gateway: Authenticates, rate limits, and routes upload, metadata and engagement calls. Playback bytes never pass through it.
- Upload service: Creates the video record, issues pre-signed multipart upload URLs, tracks chunk completion for resumability, and on completion validates the file and enqueues transcoding. Never touches the bytes itself.
- Raw store (S3 / GCS · originals): Originals as uploaded, replicated across zones. Written before any acknowledgement, retained in a cold tier after transcoding so re-encoding with a better codec is possible later.
- Transcode queue (Kafka / SQS · per chunk): One job per (video, chunk, rendition). A 10-minute video becomes hundreds of small independent jobs, which is what makes wall-clock transcoding time independent of duration. Priority lanes for popular channels.
- Transcoder fleet (ffmpeg / hardware encoders): Stateless workers that pull a job, fetch the source chunk, encode one rendition, and write the segment. Also produce thumbnails, audio tracks and subtitles. Autoscaled on queue depth; spot instances are fine because jobs are small and idempotent.
- Pipeline orchestrator (DAG per video): Splits the original into chunks at keyframes, fans out jobs, tracks completion per rendition, then stitches segment lists into manifests and marks the video ready. Handles retries and partial failure; the state machine is durable.
- Segment store (object storage · CDN origin): Transcoded segments (4 s each) per rendition, plus manifests and thumbnails, laid out as immutable objects under a content-addressed path. Origin for the CDN. Tiered: hot for recent and popular, cold for the long tail.
- Metadata service: Video and channel records: title, description, status, visibility, renditions available, duration, thumbnail ids. Serves the watch page and listing pages. Emits change events on publish and takedown.
- Metadata DB (sharded MySQL / Spanner · by video id): Source of truth for videos, channels, and visibility. Sharded by video id; channel → videos is a secondary index table. Vitess-style sharded MySQL is what YouTube actually runs.
- Metadata cache (Redis · watch page objects): Watch-page metadata for the hot set of videos. A video page is read millions of times per write, so the cache hit rate is near 100 %. Invalidated on publish, edit and takedown.
- View event stream (Kafka · key=video id): View starts, heartbeats and engagement events from players, batched by the client and acknowledged by an ingest endpoint. Feeds counting, analytics and recommendations.
- View counter (stream aggregation + dedupe): Deduplicates view events per (viewer, video, window), filters obvious bots, and maintains approximate public counts (updated every few seconds) and exact daily counts for analytics. Public counts are eventually consistent by design.
- Analytics store (ClickHouse / BigQuery): Watch time, retention curves, traffic sources per video per day. Written in batches from the stream; read by creator dashboards, never by the watch page.
User flows
- Upload a video. Bytes go straight to object storage; the services only coordinate. Resumable, durable before acknowledgement, and the pipeline starts the moment the last chunk lands.
- Client creates the video and asks for upload URLs. The video row is created in state "uploading" with the declared size and content type, so a crashed upload is visible and can be resumed or garbage-collected. The client receives an upload id and a list of pre-signed URLs, one per chunk.
- Client uploads chunks in parallel directly to the raw store, retrying any that fail. Multipart upload means no byte passes through our services and a flaky connection only re-sends the failed 16 MB part. The client stores the upload id locally so a closed tab resumes. Object storage replicates each part across zones before returning 200.
- Client signals completion; the upload service validates the assembled object. Complete the multipart upload with the part ETags, then probe the container (duration, codecs, resolution) with a lightweight header read. Reject anything that is not a video or exceeds limits. Compute a content hash asynchronously for duplicate detection and later copyright matching.
- Upload service marks the video "processing" and starts the pipeline. The state change and the pipeline start must not diverge: write the state and an outbox row in one transaction; the orchestrator consumes the outbox. If the orchestrator is down, the row waits; nothing is lost.
- Client polls or subscribes for status while the pipeline runs. Status comes from the metadata service, which reflects orchestrator progress (percent of renditions done). Creators can edit title and thumbnail during processing; publishing is allowed as soon as one low rendition is ready.
- Transcode into renditions. Split the original at keyframes, encode every (chunk, rendition) pair independently on a large stateless fleet, and stitch the results into manifests. Parallelism makes a two-hour film finish in minutes.
- Orchestrator inspects the original and splits it into chunks at keyframe boundaries. Chunks of roughly 10 s aligned to keyframes can be encoded independently without artefacts at the joins. The split is a byte-range plan, not a copy: workers read ranges of the original. The orchestrator persists the plan (N chunks × M renditions) so a restart resumes rather than restarts.
- Orchestrator fans out one job per chunk and rendition onto the queue. A 10-minute video is 60 chunks × 6 renditions = 360 jobs. Low renditions are enqueued first at higher priority so the video can publish early at 360p while 4K finishes. Jobs are idempotent: the output path is deterministic, and a re-run overwrites the same object.
- Transcoder workers pull jobs, read the chunk, encode, and write the segment to the segment store. Each job is a few seconds of CPU or under a second on a hardware encoder. Output is fragmented MP4 (CMAF) usable by both HLS and DASH, so it is encoded once. Workers are stateless and preemptible; a killed job is simply redelivered.
- Workers report completion; the orchestrator tracks progress per rendition. Completion is idempotent (set chunk 12 of 720p done). When all chunks of a rendition are done, the orchestrator verifies every segment object exists, then writes that rendition's playlist. A chunk that fails three times is re-encoded with a fallback preset; if it still fails the whole video is flagged for manual review.
- Orchestrator writes the manifests and marks renditions ready in metadata. The master manifest lists available renditions with bandwidth and resolution; the player picks among them. It is regenerated as renditions complete, so the video is playable from the first rendition. The metadata service records the rendition set and invalidates the watch-page cache.
- Popular-channel videos are pre-pushed to regional CDN shields before publish. For channels with millions of subscribers, the first minute of every rendition is warmed into regional caches so the publish-moment spike does not hit origin. Everything else is pull-through.
- Watch a video. Metadata from a cache, bytes from the CDN, quality chosen by the player. The origin is touched only on cache misses; the watch page is the same for everyone so it caches perfectly.
- Client requests the watch page metadata. One call returns everything the page needs: title, channel, duration, the manifest URL (signed if not public), thumbnail URLs and an approximate view count. Cache hit rate is near 100 % because reads dwarf writes; the metadata DB is consulted only for cold videos.
- Player fetches the master manifest and the first rendition playlist from the CDN. The master manifest is a few hundred bytes. The player starts with a conservative rendition (360p or 480p) so the first segment arrives fast, then measures throughput. Manifests are cached at the edge with a short TTL (60 s) so takedowns and new renditions propagate quickly.
- Player fetches segments sequentially, keeping a buffer of ~30 s ahead of the playhead. Segments are immutable and cached with a year-long TTL. The buffer target balances start-up latency (small first fetches) against resilience (enough buffered to ride out a throughput dip). Range requests on a single file per rendition are an alternative layout that reduces object count.
- On a CDN miss, the edge fetches from the regional shield, which fetches from the segment store once. Two cache tiers mean origin sees one request per segment per region no matter how many PoPs and viewers. Request coalescing at each tier prevents a stampede when a new video goes live. Origin egress is a small fraction of edge egress; that ratio is the CDN cost model.
- Player measures throughput and buffer, and switches rendition up or down between segments. Adaptive bitrate is a client-side decision: estimated bandwidth from the last few segment downloads plus the buffer level. Switch up only when throughput comfortably exceeds the next rendition's bitrate; switch down as soon as the buffer drains below a threshold. Segments across renditions are time-aligned so a switch is seamless.
- Player sends a view-start event and periodic heartbeats to the event stream. Batched and fire-and-forget: playback never waits on analytics. A view counts after 30 s of watch time (or the whole video if shorter), which the heartbeat stream lets us verify server-side rather than trusting the client.
- A video goes viral: 5 M concurrent viewers. One video, one manifest, 150 segments per rendition, five million players. The CDN absorbs it by design; the only origin-side work is keeping metadata and counts from becoming hot spots.
- Millions of players request the same manifest and segments across every PoP. Each PoP fetches each object once; the segment set is a few GB across renditions, easily held in every edge. 5 M viewers × 3 Mbit/s is 15 Tbit/s for this one video, served entirely by the edge.
- Regional shields coalesce misses so origin sees at most one request per object per region. A thousand PoPs missing simultaneously collapse into ~10 regional fetches per object. The segment store sees a few hundred requests per second for the whole event.
- The watch-page metadata becomes a hot cache key. Millions of reads per minute of one Redis key can saturate one shard. Replicate hot keys across shards with a suffix, and add a short in-process cache (2 s) on the metadata service so each instance answers from memory. Public watch-page JSON can also be served through the CDN with a 10-second TTL.
- View events for one video hit one Kafka partition. Key by (video_id, random 0–15) at ingest so a hot video spreads over 16 partitions; the counter job re-aggregates per video. Dedupe state per (viewer, video) for one video is 5 M entries, fine in memory for the window.
- The approximate count on the page updates every few seconds; exact counts settle later. Public counts are written to the cache from the aggregation job, not incremented per event. Creator analytics get exact, de-duplicated daily counts after bot filtering, which can lower the number: that is expected and documented.
- Takedown and visibility change. Bytes are cached in a thousand places with year-long TTLs. The design makes the manifest, not the segments, the enforcement point, and signs URLs where it matters.
- A takedown or visibility change is written to the metadata DB. Status becomes "removed" or visibility "private". This is the source of truth; everything downstream is derived from it.
- Metadata service invalidates the watch-page cache and purges the manifest from the CDN. The watch page immediately shows unavailable. The master manifest has a 60-second edge TTL and is purged by tag, so within a minute no new playback can start: without a manifest the segments are unreachable in practice.
- Segment URLs for non-public videos carry short-lived signatures, so already-issued links stop working. Private and unlisted manifests are signed per viewer with a 6-hour expiry. Public videos use unsigned URLs for cacheability; on takedown their segments remain fetchable by anyone who saved a segment URL until purged. Purge segments too for legal takedowns (a bulk purge by prefix), accepting the CDN cost.
- Sessions already playing receive a stop signal on their next heartbeat response. The heartbeat endpoint checks a small "revoked videos" set (Redis, populated by the takedown event) and tells the player to stop. Enforcement within 30 s for active sessions without any push infrastructure.
- Objects are moved to a legal-hold tier or deleted after the retention period. Deletion is asynchronous and idempotent: a job lists the video's prefix and deletes. Originals under legal hold are retained in cold storage with access logging.
Deep dives
Parallel transcoding pipeline
Why not transcode each video as one job? How do you make a two-hour upload available in minutes?
Encoding is CPU-bound and roughly real-time per rendition on a single core: a two-hour video into six renditions is twelve core-hours, so a single job takes hours. Meanwhile 500 hours arrive every minute. The pipeline must be embarrassingly parallel and must tolerate cheap, preemptible workers dying mid-job.
Video can be cut at keyframes (IDR frames) into independently decodable chunks. Encode each chunk of each rendition as a separate job, and the wall-clock time becomes the time of one chunk plus scheduling overhead, regardless of duration. The cost is an orchestration layer and care at chunk boundaries.
- Split at keyframes, one job per (chunk, rendition), orchestrator stitches chosen
- One job per (video, rendition) situational: short videos (under a minute) where the overhead of chunking exceeds the encode time
- One job per video, all renditions in one process rejected
- Managed transcoding service (AWS MediaConvert, GCP Transcoder) situational: below roughly 10 k hours per day, where running a fleet is not worth the engineering
The answer: Chunk at keyframes into ~10-second pieces, aligned across renditions so segments line up for adaptive switching. One job per (chunk, rendition), hundreds per video, on a stateless autoscaled fleet pulling from a priority queue; workers write CMAF fragments to deterministic paths, so any job can be re-run safely. An orchestrator persists the plan and progress per video (a small durable state table or a workflow engine) and writes each rendition's playlist when it completes, low renditions first, publishing the video as soon as one rendition exists. A global analysis pass (complexity per scene) feeds per-chunk bitrate targets to keep quality consistent. This is the shape of YouTube's and Netflix's pipelines.
A worker dies halfway through a chunk. What happens?
The job's lease expires (visibility timeout or Kafka rebalance) and another worker picks it up. Output paths are deterministic so the partial object, if any, is overwritten. The orchestrator only marks a chunk done on an explicit completion message that names the object, and verifies the object exists before writing the playlist. No coordination beyond that.
Uploads spike 10× for an event. What breaks?
Queue depth grows and time-to-available stretches; the fleet autoscales on depth, and spot capacity is elastic but not infinite. Priority lanes ensure short videos and large channels stay fast. Beyond that, degrade: publish at 360p first and queue high renditions for later, and defer 4K entirely during the spike. Nothing is lost; the queue is durable.
Why fragmented MP4 (CMAF) rather than separate HLS TS and DASH outputs?
One encode serves both HLS and DASH players, halving storage and encode cost. CMAF segments with byte-range addressing also let you store one file per rendition and serve segments as ranges, cutting object count by 150×. The cost is dropping very old players that only speak MPEG-TS.
How do you keep quality consistent across independently encoded chunks?
Constant-quality encoding (CRF) rather than constant bitrate, so each chunk gets the bits it needs. A cheap first pass over the whole video produces a per-scene complexity map that feeds a per-chunk bitrate cap, keeping the rendition within its declared bandwidth. Encode chunk boundaries with a small overlap and trim, so the joins are invisible.
How would live streaming change this?
The source arrives continuously, so the pipeline runs on a sliding window: encode each 2 to 4-second segment as it arrives, in parallel across renditions but sequentially in time, and append to a growing playlist. Latency is segment length times buffer depth (6 to 12 s typical; LL-HLS gets to 2 to 3 s with partial segments). Chunk-level parallelism is replaced by rendition-level parallelism, and the fleet is sized for concurrent streams rather than backlog.
Adaptive bitrate and segment size
How do you start playback in under two seconds and avoid rebuffering on a network that varies second to second?
Bandwidth to a phone can swing from 20 Mbit/s to 1 Mbit/s within a minute. A fixed-quality stream either stalls on the low end or wastes the high end. The player must pick a quality per segment, and to switch without a visible glitch the segments must be aligned across renditions.
Start-up latency is dominated by the first few fetches: manifest, first playlist, first segment. Smaller segments start faster and switch faster but multiply requests and reduce compression efficiency; larger segments do the opposite. The interview answer is a number and the reasoning.
- HLS/DASH with 4-second aligned segments, player-side ABR chosen
- Server-side ABR (server picks the quality) rejected: never for VOD; some real-time (WebRTC) systems do it with congestion feedback
- Progressive download of one file per rendition rejected: tiny clips
- WebRTC situational: interactive live (auctions, watch-together), not VOD
The answer: 4-second segments (2 s for live), keyframe-aligned across a rendition ladder of roughly 144p/200 kbit/s to 4K/15 Mbit/s in six or seven steps, exposed via a master manifest. The player starts at a low-middle rendition for a fast first frame, keeps a 30-second forward buffer, estimates throughput from recent segment downloads (harmonic mean to resist spikes), and switches up only with headroom (measured ≥ 1.5× the target bitrate) and down as soon as the buffer falls under ~10 s. Per-title encoding (a ladder tuned to each video's complexity, as Netflix does) saves 20 to 30 % of egress at the same quality. Start-up is under 2 s because the manifest and first segment total under 1 MB and are at the edge.
Why not 1-second segments for faster switching?
Each segment needs a keyframe, and keyframes cost bits: 1-second GOPs raise bitrate 10 to 20 % at the same quality. Requests per view also rise fourfold and the CDN's per-request overhead becomes visible. Four seconds is a common compromise for VOD; live uses 2 s, and LL-HLS uses partial segments to get lower latency without shrinking the GOP.
Viewers on a 2 Mbit/s link keep rebuffering. Where do you look?
First the heartbeat data: which rendition they were on when they rebuffered and what the estimate said. Common causes: the ABR is too aggressive switching up (raise the headroom factor), the ladder has a gap (add a 600 kbit/s rung), or the buffer target is too small for their jitter. Then the CDN: cache hit rate for their region and PoP latency; a miss to a far origin at the start of a segment can stall a thin buffer.
How do you measure playback quality overall?
Rebuffer ratio (stalled time / watch time), time to first frame, average bitrate delivered, and switch frequency, all from player heartbeats, aggregated by region, ISP, device and CDN PoP. Alert on rebuffer ratio per region. A/B test ABR changes with these metrics; they are the product's quality SLOs.
Does the CDN change behaviour for 4K or HDR?
No, just bigger objects: a 4K segment is ~8 MB. What changes is the share of egress: a few percent of viewers on 4K can be a third of the bytes. Per-title ladders and newer codecs (AV1 at roughly 30 % fewer bits than H.264 for the same quality) are how the cost is contained, and they are worth the extra encode CPU only for videos with enough views to amortise it.
Storage layout and tiering
Half a petabyte a day forever. How do you store it, and how do you keep the bill sane?
Storage grows without bound and views follow a steep long tail: most bytes are watched rarely after the first weeks. Storing every rendition of every video on hot, replicated storage is affordable for the head and ruinous for the tail. Object storage tiers (standard, infrequent, archive) differ in price by 5 to 20× and in retrieval latency from milliseconds to hours.
The layout must also serve the CDN as an origin efficiently, keep objects immutable so caches never go stale, and let a takedown or re-encode find everything for a video.
- Immutable objects per (video, rendition, segment), tiered by view age chosen
- One file per rendition served via byte ranges situational: with a CDN that caches byte ranges well; many large operators do this
- Keep only the original, transcode on demand rejected: a hybrid: drop the top renditions of cold videos and re-encode on demand if they get views again
- Custom distributed file system rejected: hyperscalers who own the hardware
The answer: Object storage with a path per video: seg/{video_id}/{rendition}/{n}.m4s plus manifests and thumbnails, all immutable; a re-encode writes under a new version prefix and the manifest is repointed. Originals go to the archive tier once transcoding succeeds. Renditions are tiered by a daily job on last-view time: hot for 30 days or any video with recent views, infrequent-access after that, and for videos with no views in a year, drop the 1080p and 4K renditions (re-encode from the original on demand) and archive the rest. The CDN's regional shields hold the working set, so the origin tier mostly serves first-in-region views. Erasure coding within the object store keeps durability at 11 nines at ~1.5× overhead instead of 3× replication. Expect storage to be the second-largest cost after egress and to be dominated by the tail.
A cold, archived video gets shared and starts trending. What does the viewer see?
The manifest lists only renditions that are immediately servable; archived ones are hidden. The first view triggers a restore of the hot renditions (minutes for archive tiers), or, if only the original remains, a priority re-encode of a low rendition, which finishes in under a minute for most videos. Meanwhile the viewer gets the best rendition available. The tiering job treats the spike as "recent views" and keeps it hot again.
How do you delete a video with 900 objects atomically?
You do not need atomicity; you need the manifest gone first. Remove the manifest and the metadata row (playback stops), then an idempotent async job lists the prefix and deletes in batches, retrying until the listing is empty. Legal deletion is confirmed when the listing is empty and the CDN purge has completed.
Should thumbnails and previews live with the segments?
Same store, separate prefix, always hot: thumbnails are viewed far more than videos (every listing page shows dozens) and are tiny. Generate several sizes at transcode time, serve through the CDN with long TTLs, and version the path when the creator changes the thumbnail.
How much does a new codec save and when is it worth it?
AV1 or VP9 versus H.264: 30 to 50 % fewer bits at equal quality, so proportionally less egress and storage, at 5 to 10× the encode CPU. Encode the new codec only for videos above a view threshold (the head), where the egress savings dwarf the encode cost, and keep H.264 as the universal fallback rendition. Decide per video from its view rate; that is what the large services do.
Counting views at scale
Why is the view count "approximate", and how do you count accurately for creator payouts without inflating on replay?
Five billion views a day means 60 k increments per second, concentrated on a small number of hot videos. A row-level increment per view in the metadata database is a hot-row problem at once, and a counter in Redis is fast but neither de-duplicated nor durable. Counts also feed money (creator revenue), so they attract fraud: replayed requests, bots, auto-refresh.
The observation is that there are two different products: a number on the page that must be cheap and roughly right, and a ledger for analytics that must be exact, de-duplicated and auditable but can arrive hours later.
- Stream aggregation: dedupe per (viewer, video, window), approximate public count, exact batch count chosen
- Increment a counter in the metadata DB per view rejected
- Redis INCR per view, periodic flush situational: for the live "watching now" number, where loss and approximation are fine
- Count from logs in a nightly batch only rejected: as the audit path, which we keep
The answer: Players send view_start and 30-second heartbeats to an ingest endpoint that batches into Kafka keyed by video (salted for hot videos). A stream job counts a view when a session reaches 30 s of watch time, de-duplicates on (viewer id or device hash, video, 24 h window) using state in RocksDB, and drops sessions failing cheap bot heuristics (no heartbeats, impossible rates per IP). It emits approximate totals per video every 5 s to the metadata cache (the number on the page, labelled as approximate) and exact daily aggregates to the analytics store, where a slower fraud model can retroactively adjust before payouts are calculated. YouTube shows counts frozen at 301 for a while on new videos for exactly this reason: verification lags display.
The dedupe state for a viral video is millions of entries. Does the stream job cope?
Per-key state is a (viewer, video) set with a 24-hour TTL; 5 M viewers × ~40 bytes is 200 MB for one video, fine on one task, and the salted partitioning spreads it across 16 tasks. Use a Bloom filter per (video, hour) if memory becomes an issue, accepting a small false-positive rate that under-counts slightly.
Someone writes a script that opens the video 10 000 times. What stops the count?
Dedupe by viewer or device within 24 h removes repeats from one identity; per-IP rate limits and the heartbeat requirement remove bots that do not actually play; and the batch fraud model catches distributed attacks by pattern (many new devices from one ASN, uniform watch times). Views are tentatively counted and later removed, so the public number can go down.
Why not count the view in the CDN logs instead of client events?
CDN logs tell you a segment was fetched, not that it was watched, and prefetching and bots make that noisy. Client heartbeats give watch time, position, rebuffering and rendition, which you need for analytics anyway. Use CDN logs as a cross-check for gross discrepancies.
How do you show "watching now" for a live stream?
A per-video HyperLogLog or a simple counter in Redis updated by heartbeats with a 60-second expiry window; read every few seconds by the page. Approximate, lossy, and separate from the durable view count.
Metadata storage and the watch page
What holds video and channel metadata, how is it sharded, and why does the watch page not hit it?
Metadata is small (a few KB per video, billions of videos) but read at enormous rates: the watch page, listings, embeds, and the player all need it. Writes are rare: an upload, an edit, a status change. This is the classic read-heavy relational workload with a few secondary access patterns (videos by channel, videos by status for moderation).
The system of record needs transactions (publish and visibility changes must be atomic with the outbox) and secondary indexes, which argues for relational storage; the read rate argues for a cache in front and a CDN in front of that.
- Sharded relational (Vitess/MySQL or Spanner) by video id, Redis cache, CDN for public JSON chosen
- Wide-column store (Cassandra) keyed by video id situational: if writes were high or multi-region writes were required; neither is true here
- Document store situational: a reasonable alternative; not a differentiator
- Single Postgres with replicas rejected: a smaller platform (tens of millions of videos)
The answer: A videos table sharded by video id (random 64-bit id, so shards are uniform), a channel_videos index table sharded by channel id and written in the same transaction via the outbox relay, and a status index for moderation queues. Redis holds the rendered watch-page object per video with a 1-hour TTL and explicit invalidation on edit, publish and takedown; hot keys are replicated across shards. Public watch-page JSON is additionally cacheable at the CDN for 10 s, which absorbs viral spikes before they reach Redis. Reads of a creator's own video go to the primary for 5 s after an edit so they see their change (read-your-writes). Counts are not stored in this table; they come from the counter pipeline into the cache.
A creator edits the title and still sees the old one. Why, and what is the fix?
They hit the cache or a replica before invalidation propagated. Fix: after a write, invalidate the cache synchronously before returning, and route the writer's reads to the primary for a few seconds via a short-lived cookie or header. Other viewers may see the old title for up to the CDN TTL, which is acceptable.
How do you list a channel's 20 000 videos newest-first?
From the channel_videos index table keyed by channel id and ordered by a time-ordered video id (or published_at), with cursor pagination. It is written through the outbox when a video publishes, so it can lag the primary by a second. Never scan the videos table across shards.
Where do comments and likes live?
Comments in a separate store keyed by video id, ordered by time, with a per-video partition (Cassandra or a sharded MySQL table by video id): append-heavy, read as a page per video. Likes are a counter problem like views: events into the stream, aggregated counts into the cache, and a per-user like set in Redis or a keyed table for the "you liked this" state.
How would you make the watch page survive a metadata outage?
The CDN serves stale cached JSON (stale-if-error), the player has the manifest URL and plays from the CDN without any service, and view events buffer client-side. Only new uploads, edits and cold videos fail. Playback availability is decoupled from the control plane by design.