Caso 3 · Video

Streaming de video tipo YouTube

Subir, transcodificar, almacenar y reproducir videos en cualquier dispositivo y red, con baja latencia y alto throughput.

Recursos

Video, UploadSession, ManifestHLS, Channel, ViewEvent.

Operaciones

Upload directo a storage, transcoding async, lectura por CDN.

Escala

Petabytes de blobs, miles de horas/min subidas, billones de plays.

Riesgo

Servir desde app, costos de storage, derechos de copyright.

Requisitos

Aclara qué se debe hacer y con qué garantías de calidad.

Funcionales

  • Upload de video con resume de chunks.
  • Transcoding a múltiples renditions y thumbnails.
  • Playback adaptativo (HLS / DASH) por dispositivo.
  • Search, comentarios, likes y subscripciones.
  • Reportes / takedown por copyright (DMCA).

No funcionales

  • TTFB < 500 ms para reproducción global.
  • Durabilidad 11×9 en object storage.
  • 99.95% disponibilidad de reproducción.
  • Costos de egress bajo control vía CDN.
  • Eventual consistency en views/likes.

Cómo aclarar requisitos en la entrevista

Casos de uso típicos. VOD largo (películas, documentales, vlogs), Shorts (videos verticales < 60 s), live streaming (gaming, eventos, deportes), premiere programado, contenido restringido por edad o región, monetización con ads (pre-roll, mid-roll, post-roll), creator dashboard con analytics, family-safe vs maduro.

Preguntas que debes hacer:

• ¿Solo VOD o también live? Live cambia todo: low-latency HLS, ingest, chat, etc.
• ¿DRM obligatorio (Widevine / FairPlay) o solo signed URLs?
• ¿Comments y likes están en este alcance?
• ¿Subtítulos automáticos generados (ASR)?
• ¿Geo-blocking por país (licencias)?
• ¿Monetización: ads, paywall, ambos?
• ¿Privacidad: público / unlisted / privado / paywall?
• ¿Edad / política de contenido: review humana o solo automática?
• ¿Recommendations en este alcance o se asume otro sistema?

Frase clave: "Asumo VOD público con monetización via ads, sin live ni DRM. Si me pides live, agrego ingest server con low-latency HLS y un servicio de chat al final."

Estimaciones de capacidad

Resoluciones año/día/min/seg. Lo que justifica object storage + CDN y no servir desde el app server.

2B
MAU
Plataforma global.
~500M DAU · ~6K logins/s avg.
500 h/min
Subidas
~30K h/h · ~720K h/día.
~262M h/año · ~30 PB/mes nuevos.
~1B h/día
Watch time
~365B h/año · ~42M h/h.
Bandwidth dominado por CDN.
~300K QPS
Watch events
~26B/día · ~9.5T/año.
Picos tarde-noche por timezone.
~1 EB
Storage total
+~30 PB/mes nuevos.
Activos calientes + archives + thumbs.
~5
Renditions / video
240p · 480p · 720p · 1080p · 4K.
+ audio-only para podcasts.
95%+
Cache hit CDN
Métrica clave del costo.
~5% miss = ~50 PB/mes a origen.
99.95%
SLO playback
~4.4 h/año · ~21 min/mes.
~43 s/día permitidos.

Endpoints

Subida con URL firmada (no pasa por tu app), reproducción vía CDN.

MétodoPathDescripciónNotas
POST/api/videos/upload-urlPide URL pre-firmada para subir directo a object storage.S3/GCS multipart.
POST/api/videos/{videoId}/complete-uploadMarca el upload terminado y dispara el transcoding.Encola job.
GET/api/videos/{videoId}Metadata: título, descripción, estado, thumbnails.Cacheable.
GET/api/videos/{videoId}/manifest.m3u8Manifiesto HLS con renditions.Vía CDN.
POST/api/events/viewRegistra play/progreso.Async, batch.

Cómo defiendo estas APIs en la entrevista

El upload no toca mi backend. Es la decisión más importante de las APIs: POST /videos/upload-url devuelve URLs pre-firmadas; el cliente hace PUT directamente al object storage. Mi servicio solo gestiona metadata. Sin esto, sería imposible mantener 500 horas de uploads/min con un app server.

Async by default para work pesado. complete-upload devuelve 202 Accepted con un job ID, no 200 con video listo. El transcoding tarda minutos — no puedo dejar al cliente colgado. El playback usa GET /videos/{id}/manifest.m3u8 que va a CDN; otra vez, mi backend no toca bytes.

Versionado y observabilidad. URL path /v1. Status codes específicos: 201 al crear video metadata, 202 al disparar transcoding, 423 Locked si video bajo DMCA review, 410 Gone si fue removido. Cada response trae correlation-id en header para tracing.

Frase clave: "El upload va por pre-signed URL — el cliente sube directo al storage. El playback va por CDN — los bytes nunca tocan mi backend. Mi API solo orquesta metadata y eventos."

Ejemplo: subida

El cliente sube directo al storage; el backend sólo da credenciales.

POST /api/videos/upload-url
{ "filename": "vacation.mov", "sizeBytes": 524288000 }

201 Created
{
  "videoId": "v_9981",
  "uploadUrls": [
    "https://storage/.../part1?sig=...",
    "https://storage/.../part2?sig=..."
  ],
  "expiresAt": "2026-05-01T13:00:00Z"
}

Arquitectura

flowchart LR Client(["Cliente Web/Móvil"]) -->|pre-signed URL| API Client -->|chunks directo| Store API{{"🌐 API GATEWAY + API Service
Auth + Rate Limit"}} --> Meta API -->|complete-upload| Stream Stream --> Trans["Transcoder Workers
FFmpeg async"] Trans --> Store Trans --> Meta Trans --> Thumb["Thumbnail Generator"] Client -- playback request --> Edge(["CDN Edge POPs"]) Edge -->|miss| Store Client -. events .-> Analytics subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["videos/raw"] s2["videos/hls"] s3["thumbnails"] end subgraph Meta["💾 Metadata DB · Postgres / Spanner"] direction TB m1["videos"] m2["video_assets"] m3["channels"] m4["comments"] m5["subscriptions"] end subgraph Stream["📨 Pub/Sub"] direction TB k1["video.uploaded"] k2["video.transcoded"] end subgraph Analytics["🗄️ Analytics · BigQuery"] direction TB a1["view_events"] a2["watch_time_daily"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#7f1d1d,stroke:#fb7185,stroke-width:2px,color:#fecdd3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class API gateway class Edge edge class Trans,Thumb service class Client client

Cómo lo explico en la entrevista

Upload. El creador presiona "subir". El cliente pide POST /api/videos/upload-url; el API responde con un videoId y una pre-signed URL multipart al object storage. El cliente sube los chunks directo a S3/GCS — los bytes nunca tocan mi backend. Cuando termina, hace POST /api/videos/{id}/complete-upload y eso publica un evento video.uploaded al Pub/Sub.

Transcoding async. Los Transcoder workers (auto-scaled) consumen el evento, descargan el master, generan renditions (240p, 480p, 720p, 1080p, 4K) más segmentos HLS y thumbnails con FFmpeg, y los suben de regreso al storage. Updatean la metadata DB cambiando estado de processing a ready.

Playback. El viewer hace play. Cliente pide GET /api/videos/{id}/manifest.m3u8. El edge CDN responde si lo tiene cacheado; si no, va al origen y popula. El cliente pide segmentos de 2–10 s al CDN POP más cercano. Mi backend nunca toca los bytes; eso es lo que hace que esto escale a billones de horas/día.

Frase clave: el truco es que el upload va por pre-signed URL y la entrega va por CDN. Mi backend solo gestiona metadata y orquestación de jobs — los blobs pesados los maneja la infra de almacenamiento y entrega.

Por qué cada componente

Pre-signed URL multipartResumable, paralelizable, ahorra ancho de banda. El cliente sube directo a storage.
Object Storage (S3/GCS)11×9 durabilidad, escala a EB sin operar discos.
Pub/Sub video.uploadedDesacopla la confirmación del upload del trabajo pesado de transcoding.
Transcoder workersAuto-scaling según cola. Un worker procesa 1 video a la vez, paralelismo natural.
Múltiples renditionsAdaptive bitrate. El cliente elige la calidad según red y dispositivo, sin re-encoding.
Metadata DBJoins para search, comments, channels. Indexable por title, tags, etc.
CDN edge POPs95%+ cache hit ratio define el costo del negocio. Latencia < 100 ms global.
Analytics pipelineView events alimentan recos, trending, billing y royalties — sin presionar operacional.

Cuellos de botella

  • Encoding queue lag en eventos virales o uploads masivos.
  • Cold cache para videos nuevos virales — primer impacto pega al origen.
  • Search index lag detrás de uploads recientes.
  • Egress cost cuando hay miss en CDN (clip que no estaba caliente).
  • Long-tail storage: 99% de videos nunca se ven después del primer mes pero ocupan EB.

Mejoras / cómo escala

  • Tiered encoding: 480p disponible en segundos, resto en background.
  • Cache prewarm al detectar trending por geo / canal popular.
  • Per-segment caching más fino, no todo el video.
  • Cold storage tier (Glacier) para videos sin views > 90 días.
  • Two-pass encoding en videos con alto watch time (mejor quality/byte).
  • P2P assist (CDN edge serving via clients) para eventos en vivo masivos.

Por qué estos servicios cloud (vs alternativas)

S3 / GCS / Azure Blobobject storage

Por qué: 11×9 durabilidad nativa, multipart upload con resume, lifecycle policies (hot → cold → archive automático), pre-signed URLs para upload directo desde el cliente. Es el patrón estándar para blobs masivos.

vs HDFS / MinIO self-hosted: tienes que operar el cluster, manejar disk failures, replication. vs servir desde un filesystem en VM: imposible a escala, no escala horizontal. vs Cassandra / DynamoDB: no diseñados para blobs > 1 MB.

Pub/Sub / Kafkavideo.uploaded events

Por qué: desacopla el upload del transcoding pesado. Los workers consumen al ritmo que pueden, autoscaling por backlog. Permite re-procesar (re-encode) si cambias de codec.

vs SQS: queue point-to-point — no soporta múltiples consumers consumiendo el mismo evento (transcoder + analytics + content moderation). vs llamada síncrona al transcoder: el upload se queda bloqueado minutos.

CloudFront / Cloud CDN / Akamai / Cloudflarevideo delivery

Por qué: POPs globales, latencia <100 ms global, cache hit ratio del 95%+ define el costo del negocio. Adaptive bitrate funciona naturalmente (cada segmento es una URL cacheable).

vs servir desde object storage directo: egress costaría 10–100×, latencia inaceptable fuera de la región. vs CDN self-hosted: imposible a escala global. vs P2P only: falla en eventos cold (no hay peers todavía).

Transcoder workers (FFmpeg)media processing

Por qué: FFmpeg es estándar de industria para transcoding. Lo corres en Kubernetes / ECS / Cloud Run con autoscaling según cola pendiente. Un worker = un video a la vez, paralelismo natural.

vs MediaConvert (AWS) / Transcoder API (GCP): managed services aceptables pero más caros y menos flexibles para custom encoding ladders. vs hardware encoders dedicados: rentables solo a escala extrema.

Metadata DB (Spanner / Postgres / Cassandra)video metadata

Por qué: lecturas masivas con joins (video + channel + comments), índices full-text para search, ACID en operaciones de canal/upload. Postgres/Spanner para joins, Cassandra si write-heavy y multi-region.

vs DynamoDB: sin joins ni full-text — tendrías que duplicar data. vs MongoDB: aceptable pero performance se degrada con joins via $lookup.

BigQuery / Snowflakeanalytics warehouse

Por qué: serverless OLAP, escala a EB sin provisioning. Eventos de view/click se sinkan vía Dataflow / Streaming. Reportes para creators (analytics dashboard) y ML (training pipelines).

vs ClickHouse: aceptable, más barato pero requiere ops. vs Redshift: requiere provisioning capacity. vs Postgres: ni de cerca a esta escala.

Diseño de base de datos

Metadata en DB relacional, blobs en object storage, eventos analíticos en stream.

videos
video_idUUID PK
channel_idUUID FK
titleTEXT
descriptionTEXT
statusENUM
duration_sINT
visibilityENUM
created_atTIMESTAMPTZ
idx (channel_id, created_at DESC)
idx (status)
video_assets
asset_idUUID PK
video_idUUID FK
renditionENUM (240p…4K)
codecVARCHAR
storage_urlTEXT
size_bytesBIGINT
idx (video_id, rendition)
channels
channel_idUUID PK
handleVARCHAR UNIQUE
nameTEXT
subscribersBIGINT
idx (handle UNIQUE)
view_events (stream)
event_idUUID
video_idUUID
user_idUUID
watched_sINT
device, country
Tópico Kafka particionado por video_id
Sink: BigQuery / data lake

Cómo defiendo este diseño de base de datos

Separación radical: metadata vs blobs vs events. Tres stores distintos: una DB transaccional para video metadata (joins, indexes), object storage para los blobs, stream para events. Esta separación es la que hace que el sistema escale a EB de storage y miles de millones de horas vistas/día.

Schema mínimo en metadata. En videos: campos de búsqueda y display, no los bytes. video_assets separado guarda referencias a las renditions en object storage — el cliente pide la rendition adaptive, mi DB solo dice "vive en S3://bucket/path". Index (channel_id, created_at DESC) para "videos del canal", (status) para filtrar processing/ready.

Eventos a stream, no a la DB. View events son ~300K/s — cualquier DB tradicional muere intentando escribir eso. Stream particionado por video_id mantiene orden por video; el sink agrega en BigQuery para analytics. La DB solo se entera de un view via batch que actualiza counters cada minuto.

Frase clave: "Mi DB de metadata es small (PB), el object storage es huge (EB), y el stream es high-throughput. Cada uno hace lo suyo; la falla de uno no rompe los otros."

Decisiones técnicas

TemaDecisiónPor qué
UploadURL pre-firmada con multipart.Ahorra ancho de banda en tu app y permite reanudar.
TranscodingJob async por cola con varios workers.El usuario no espera; se generan múltiples renditions.
EntregaHLS/DASH con segmentos pequeños vía CDN.Adaptive bitrate y caching natural en edge.
MetadataDB relacional o NoSQL para video, comentarios y estados.Lecturas masivas, escribibles bajo control.
ThumbnailsGenerados en el pipeline de transcoding.Una sola pasada, costo amortizado.
Eventos de playStream a un pipeline analítico.Para recomendaciones, billing y trending.

Cómo defiendo estas decisiones técnicas

Decisión #1: pre-signed URL para upload. El constraint es 500 h/min de uploads. Si pasaran por mi backend, necesitaría miles de servidores solo para recibir bytes. Pre-signed URL: el cliente sube directo a S3, mi backend solo entrega credenciales con expiración corta (15 min).

Decisión #2: transcoding async con cola. El constraint es duración variable del transcoding (10 s para Shorts, 30 min para 4K largo). Sync mata UX. Cola con autoscaling: workers procesan según backlog, el cliente recibe 202 y consulta estado o usa webhook al completar.

Decisión #3: HLS/DASH con segmentos cortos. El constraint es adaptive bitrate y caching. Segmentos de 2–10 s son cacheables por CDN, soportan adaptive (cliente cambia bitrate por segmento), y permiten failover de un POP a otro mid-playback.

Métricas que validan. Cache hit ratio del CDN > 95% (define costo de egress). Encoding queue lag < 5 min (a 60 min, los uploads parecen rotos). TTFB < 500 ms global. Buffer ratio (re-buffering events / play events) < 1%.

Cuándo cambio de opinión. Live streaming → cambio HLS por LL-HLS (low latency) y agrego ingest server. Free tier explosivo → más agresivo el archive a cold storage. Si llega 4K mainstream → más renditions, más codec efficiency (AV1).

Frase clave: "Cada decisión es un trade entre latencia, costo y simplicidad. Pre-signed URL costa una llamada extra pero ahorra petabits/s de bandwidth. Transcoding async cuesta UX inicial pero hace que el sistema sea operable."

Pitfalls

Servir blobs desde tu app

Saturarás el server. Siempre object storage + CDN.

Transcoding síncrono

Hace que el upload tarde minutos. Cola y notificación cuando termine.

Costos de egress

El CDN es caro a escala. Usa cache hit ratio como métrica clave.

Script de respuesta: “El upload va directo al object storage usando una URL pre-firmada que entrega mi API. Cuando el cliente confirma, encolo un job de transcoding que produce HLS con varias renditions. La metadata vive en una DB; los segmentos los entrega un CDN. La reproducción consume el manifest desde edge, así mi backend no toca el blob nunca.”

Tiers de almacenamiento

Aquí los tiers son todo: long-tail de videos puede vivir frozen indefinidamente. Esto define el costo total del negocio.

Hot · CDN + RAM
Top 1% de videos (trending, virales) cacheados en CDN POPs y manifests en Redis.
CloudFront / Cloud CDN + Redis
~$0.02–0.08/GB egress + ~$36/GB RAM
Warm · object storage Standard
Videos populares (último mes), todos los renditions HLS, thumbnails activos.
S3 Standard / GCS Standard
~$0.023/GB/mes
Cold · object storage IA
Long-tail (videos sin views > 90 días), renditions no comunes (4K, codec avanzados poco usados).
S3 Standard-IA / GCS Nearline
~$0.0125/GB/mes · retrieval $0.01/GB
Frozen · archive
Master files originales (raw upload), videos > 5 años sin views, archive de creators inactivos.
S3 Glacier / GCS Coldline · Glacier Deep Archive para masters
~$0.004/GB/mes (Glacier) · ~$0.001 (Deep)

Cómo defenderlo en la entrevista

Long-tail es el rey aquí. 99% de los videos en YouTube tienen < 100 views/mes. Mantenerlos en S3 Standard es gastar millones extra al mes. Lifecycle a IA tras 30 días sin views los baja al 50% del costo.

Master files siempre frozen. El upload original (4K master) se guarda por si necesito re-encode con un codec mejor (AV1, HEVC). Pero ese master se toca casi nunca → Glacier Deep Archive desde día 1. Si necesito re-encode, pago 12 h de retrieval (es batch process anyway).

Re-promotion automática. Si un video viejo de pronto recibe views (alguien lo enlazó), un trigger lo promueve de IA → Standard. Sin esto, el primer view sufre 100 ms extra de latencia.

Frase clave: "Sin storage tiering el sistema no es viable económicamente. Los tiers reducen el costo total ~10× y los lifecycle policies hacen que sea automático. CDN + Hot tier sirven el 95% del tráfico desde el 1% del storage."

Versión alternativa: con Load Balancer (no API Gateway)

Para video, la latencia importa pero el bytes mismo va por CDN; el LB solo gestiona metadata.

flowchart LR Client(["Cliente Web/Móvil"]) --> Edge(["CDN POPs (segments)"]) Client --> LB LB{{"⚖️ L7 LOAD BALANCER
ALB / Cloud LB"}} --> Pod["API Pods
+ JWT auth middleware
+ Rate limit Redis"] Pod --> Meta Pod -->|complete-upload| Stream Stream --> Trans["Transcoder Workers"] Trans --> Store Edge -->|miss| Store Client -->|pre-signed URL| Store Client -. events .-> Analytics subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["videos/raw"] s2["videos/hls"] s3["thumbnails"] end subgraph Meta["💾 Metadata DB"] direction TB m1["videos"] m2["video_assets"] m3["channels"] m4["comments"] end subgraph Stream["📨 Pub/Sub"] direction TB k1["video.uploaded"] end subgraph Analytics["🗄️ Pipeline analítico · BigQuery"] direction TB a1["view_events"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#7f1d1d,stroke:#fb7185,stroke-width:2px,color:#fecdd3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Edge edge class Pod,Trans service class Client client

API Gateway vs Load Balancer: cómo defender cada uno

YouTube tiene público y privado. La API pública (Data API v3) con developer keys, quotas, plans → Gateway encaja perfecto. La API interna entre tu frontend y backend → LB es más simple y rápido. En producción, ambos coexisten (Gateway en path /v3/, LB en path interno).

Frase clave: "Para Data API pública necesito Gateway: developer keys, monetización, quotas, métricas por endpoint. Para el playback y dashboard de creators (frontend propio), LB con auth en pod es más rápido y barato. En la práctica YouTube usa ambos en paralelo."

Cuándo elegir API Gateway

  • Data API pública (developer keys, quotas, monetización por tier).
  • Multiple SDKs (web, móvil, smart TV, partners) con políticas distintas.
  • OAuth flows para "Sign in with YouTube".
  • Rate limits granulares por endpoint (uploads vs lookups).
  • Centralizar versioning (/v3/, /v4/).
  • WAF y bot detection integrados.

Cuándo elegir Load Balancer

  • API interna entre tu frontend y backend.
  • Latencia crítica en endpoints de hot path (manifest fetch, watch events).
  • Throughput masivo (~300K events/s) — gateway sería caro.
  • Auth simple: cookie de sesión propia o JWT verificado local.
  • Service mesh ya cubre auth/observability con sidecar.
  • Eventos analíticos: ingest endpoint masivo, no necesita features de gateway.