Caso 5 · Feed ranking

TikTok: feed personalizado infinito

Cada usuario abre la app y recibe videos altamente relevantes, en menos de 200 ms, con scroll infinito y feedback continuo.

Recursos

User, Video, FeedSession, Event (watch/like/share).

Operaciones

Generar feed, registrar interacciones, refrescar candidatos.

Escala

Cientos de millones DAU, miles de eventos/seg por usuario activo.

Riesgo

Cold start, duplicados, eco chamber, latencia del modelo.

Requisitos

Lo que firmas: feed personalizado, fresh y rápido en cualquier red.

Funcionales

  • Feed personalizado con scroll infinito.
  • Eventos de watch, like, share, follow.
  • Refresh manual (pull-to-refresh).
  • For You vs Following tabs separados.
  • Diversidad y exploración en el ranking.

No funcionales

  • Feed p99 < 200 ms.
  • Frescura < 1 min para creators populares.
  • 99.95% disponibilidad global.
  • No duplicados dentro de una sesión.
  • Aprende rápido: feedback < segundos.

Cómo aclarar requisitos en la entrevista

Casos de uso típicos. Apertura de app (cold start del feed), scroll continuo durante 30 min en commute, search de hashtag o sound, perfil de creator y sus videos, live stream desde feed, duets / stitches con video original, brand-sponsored content, notificaciones push de creator favorito.

Preguntas que debes hacer:

• ¿Solo For You o también Following / Friends / LIVE?
• ¿Long-form (videos > 1 min) en el mismo feed?
• ¿Duets / Stitch en alcance? Cambian el grafo de items.
• ¿Live streams en el feed o aparte?
• ¿Privacy: cuenta privada filtra out el feed público?
• ¿Ads intercalados como video natural?
• ¿Cold start: usuario nuevo, cómo arranca el feed?
• ¿Multi-cuenta o solo una sesión?
• ¿Métrica de éxito: completion rate, watch time, retención D7?

Frase clave: "Voy a asumir feed For You único, sin live ni ads, video corto vertical. Si quieres long-form o live lo agrego como extensión, pero la arquitectura base no cambia."

Estimaciones de capacidad

Resoluciones año/día/seg. El número que define todo: feeds servidos por segundo y ratio recall/rank.

1B
DAU
~3B MAU · plataforma global.
~12K logins/s avg.
~50
Videos/user/día
~50B feed views/día.
~18T views/año · ~600K/s avg.
~600K QPS
Feed lecturas
~50B/día · ~18T/año.
~6M QPS pico · 10× sobre avg.
~10K/s
Eventos por usuario
~864M watch/like/share por user×día.
~360G eventos/año.
~500
Candidates → 10–15 ranked
~3T candidates rankeados/día.
2-stage retrieval mantiene budget.
~1 KB
Feature vector / item
~100M items × 1 KB = ~100 GB.
+ user features ~1 KB × 1B = ~1 TB.
~100 ms
Budget ranker p99
~30 ms recall + 50 ms rank + 20 ms diversity.
Total response < 200 ms p99.
99.95%
SLO feed
~4.4 h/año · ~21 min/mes.
~43 s/día permitidos.

Endpoints

MétodoPathDescripciónNotas
GET/api/feed?cursor=...&limit=10Devuelve siguientes videos rankeados.Cursor opaco.
POST/api/events/watchRegistra duración vista, completion rate.Batch async.
POST/api/events/likeLike/unlike.
POST/api/events/shareShare externo.
GET/api/videos/{videoId}Detalle hidratado del video.Cacheable.
POST/api/feed/refreshForzar regeneración de candidatos.Pull-to-refresh.

Cómo defiendo estas APIs en la entrevista

Cursor opaco es la decisión clave. No expongo offset numérico — el cursor codifica sesión, experiment_id, recent_seen y posición lógica. Si re-deploy del ranker o cambio de experimento entre requests, el cursor sobrevive. Si el cliente lo manipula, lo invalido y arranco fresh.

Eventos como fire-and-forget. POST /events/watch devuelve 202 inmediato — no bloquea el feed scrolling. Cliente bufferea localmente y batchea cada 5 s o 10 events. Backend ack rápido y la analytics se procesa async.

Detalle del video bajo demanda. El feed devuelve solo videoId + score; el cliente pide GET /videos/{id} en lazy load mientras hace scroll. Esto mantiene el feed payload pequeño (~1 KB para 10 items vs ~100 KB con metadata completa).

Frase clave: "Las APIs son ligeras: feed devuelve IDs rankeados con cursor, eventos van fire-and-forget, detalles se hidratan lazy. Un GET de feed son ~30ms de network round-trip."

Ejemplo: feed

GET /api/feed?cursor=eyJzZXNzaW9uIjoiczFiIn0&limit=5

200 OK
{
  "items": [
    { "videoId": "v_a1", "score": 0.92, "reason": "trend" },
    { "videoId": "v_b7", "score": 0.88, "reason": "creator" },
    { "videoId": "v_c4", "score": 0.85, "reason": "topic" }
  ],
  "nextCursor": "eyJzZXNzaW9uIjoiczFiIiwib2Zmc2V0Ijo1fQ"
}

Arquitectura

flowchart LR Client(["App / Móvil"]) -->|GET /feed| Feed Feed{{"🌐 FEED API GATEWAY
cursor token"}} --> CG["Candidate Generation
trending + follow + ANN"] CG --> Trend CG --> Graph CG --> ANN Feed --> Rank["Online Ranker
~100 ms p99"] Rank --> FS Feed --> Diverse["Diversity Filter
creator/topic mix"] Client -. eventos .-> Stream Stream --> FS Stream --> Train["Training Pipeline
model refresh"] Train --> Rank Client -- video bytes --> CDN(["CDN POPs"]) subgraph Trend["💾 Trending Cache · Redis"] direction TB t1["trending:region"] t2["trending:topic"] end subgraph Graph["💾 Follow Graph · Cassandra"] direction TB g1["follows"] g2["videos"] end subgraph ANN["🧠 Embeddings ANN · HNSW / FAISS"] direction TB v1["video_vectors"] v2["user_vectors"] end subgraph FS["💾 Feature Store · Bigtable / Redis"] direction TB f1["user_features:id"] f2["item_features:id"] f3["recent_seen:id"] end subgraph Stream["📨 Kafka · watch/like/share"] direction TB k1["watch_events"] k2["like_events"] k3["share_events"] 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:#581c87,stroke:#f472b6,stroke-width:2px,color:#fce7f3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class Feed gateway class CDN edge class CG,Rank,Diverse,Train service class Client client

Cómo lo explico en la entrevista

Apertura. El user abre la app y hace GET /api/feed?cursor=.... El cursor es un token opaco que codifica sesión + offset + experimento. Esto sobrevive a re-rankings y cambios de modelo. El Feed API dispara dos cosas en paralelo: candidate generation y feature lookup del user.

Candidate generation. Combino tres fuentes para llegar a ~500 candidatos: (1) trending por región/país desde un cache caliente, (2) follow graph con uploads recientes de creators que sigues, (3) vecinos en embeddings via ANN (HNSW/FAISS) sobre lo que ya viste. La lectura debe estar en ~30 ms.

Online ranker. Esos 500 candidatos pasan al ranker. Trae las features pre-calculadas del feature store para user e items, las pasa al modelo (TF Serving o ONNX) y devuelve scores. Aplico filtros: policy (no NSFW, locale match), diversity (no stack del mismo creator, mezcla de topics), recent_seen dedup. Top 10–15 al cliente. Total: ~100 ms p99.

Feedback loop. Mientras user mira, cada watch / like / share / share emite evento al Kafka. El stream alimenta features online (fast: segundos) y el training pipeline (batch: cada hora o día) para refrescar el modelo.

Frase clave: es un pipeline de dos etapas — recall (mucho candidate, barato) más ranking (pocos, caro) — con un feedback loop que aprende rápido sin entrenar en el path crítico.

Por qué cada componente

Cursor opacoSobrevive a cambios de ranking y deploys. El cliente no debe entender qué hay dentro.
Two-stage retrievalCosto y latencia: rankear millones es imposible; recall barato + rank caro es óptimo.
ANN (HNSW / FAISS)Top-K en embeddings sub-10 ms. Esencial para "similar a lo que viste".
Feature StoreTrain/serve consistency: la misma feature en entrenamiento y en inferencia. Evita skew.
Online rankerModelo entrenado offline, servido online. Estabilidad de latencia y A/B sano.
Diversity filterSin esto el feed se aburre y hay eco chamber. Mezcla creators y topics.
recent_seen dedupSET en Redis con TTL por sesión. Evita ver el mismo video dos veces.
Kafka eventsFeedback en segundos al feature store. Reprocesable para training.

Cuellos de botella

  • Feature store hot keys: items virales son leídos por todos.
  • Cold start de users sin historial — ANN sobre embedding vacío.
  • Model serving en GPU saturado durante peak.
  • Feature lookup latency: típicamente el bottleneck más grande.
  • Embedding refresh lag para items nuevos (no aparecen en ANN inmediatamente).

Mejoras / cómo escala

  • Co-locate features con ranker (sidecar) para reducir RTT.
  • Bandit con epsilon-greedy en cold start: forzar exploración 5–10%.
  • Modelo lighter en path crítico; modelo heavier ofrece refresh batch.
  • HNSW incremental updates para reflejar uploads en minutos.
  • Pre-warming de candidates de viajes/sesiones esperadas.
  • Edge inference para queries simples sin round-trip al ranker central.

Por qué estos servicios cloud (vs alternativas)

FAISS / HNSW / ScaNNembeddings ANN

Por qué: retrieval de top-K vecinos en embeddings de ~100M videos en sub-10 ms. HNSW soporta updates incrementales (videos nuevos aparecen rápido). Recall > 95% con tuning.

vs Postgres + pgvector: insuficiente a 100M items y 600K QPS. vs Elasticsearch dense_vector: aceptable pero menos óptimo en recall/latencia. vs Vertex AI Matching Engine: managed equivalente (GCP-only).

Apache Kafkaevents watch/like/share

Por qué: 10K+ events/s por usuario activo. Particionado por user_id mantiene orden de sesión. Stream alimenta feature store online (rápido) y training pipeline (batch).

vs Kinesis: AWS-only, throughput por shard limitado. vs Pub/Sub: menos control de retention/order. vs SQS: queue, no escala a múltiples consumers independientes.

Feature Store (Bigtable / DynamoDB / Redis)user + item features

Por qué: bulk MGET de ~500 items en <30 ms para alimentar el ranker. Bigtable para volumen, Redis para hot keys frequently read.

vs Postgres: sin chance a 6M req/s. vs DynamoDB: aceptable, más caro a este volumen. vs Cassandra: aceptable pero tunable más complejo.

Model Serving (TF Serving / Triton / ONNX Runtime)online ranker

Por qué: serve inference con GPU/CPU optimized, batching de requests, model versioning, A/B sin redeployar el código de la app.

vs SageMaker / Vertex AI: managed, más caro y menos custom batching. vs FastAPI con torch.load: sin batching ni warm-up adecuado, latencia inaceptable.

CDN (CloudFront / Cloud CDN / Akamai)video bytes

Por qué: entrega de video por POPs globales. Pre-fetching del siguiente video en feed reduce buffering. Egress cost contenido por cache hit alto.

vs servir desde object storage: latencia y costo prohibitivo. vs P2P only: falla en cold start de videos virales.

Diseño de base de datos

Metadata en relacional, feed materializado en cache, eventos a stream para feature store.

videos
video_idUUID PK
creator_idUUID FK
captionTEXT
topicsTEXT[]
duration_sINT
created_atTIMESTAMPTZ
idx (creator_id, created_at DESC)
idx GIN (topics)
follows
follower_idUUID PK
followee_idUUID PK
created_atTIMESTAMPTZ
idx (followee_id, follower_id) para fan-out reverso
feature_store (online)
user_features:{id}HASH
item_features:{id}HASH
recent_seen:{id}SET
Backed por Redis / DynamoDB low-latency
TTL en recent_seen para evitar duplicados
events (stream + sink)
user_idUUID
video_idUUID
actionENUM
watched_pctSMALLINT
tsTIMESTAMPTZ
Tópico Kafka particionado por user_id
Sink: feature store + ML training

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

Tres dominios, tres stores. Catálogo (videos, follows) en relacional para joins; feature store online (Redis/Bigtable) para sub-30ms feature lookup; embeddings en ANN index para retrieval. Mezclarlos sería ineficiente: la DB no escala a 600K QPS de feature lookups, y el ANN no puede joinear con users.

Follows con índice reverso. Para fan-out reverso ("qué creators sigue este user → qué videos nuevos hay"), el índice (followee_id, follower_id) permite query rápida. Tabla bidireccional con denormalización si los reads son extremos.

Feature store con TTL. recent_seen:{user} es SET con TTL 4 h — al expirar, los videos vuelven a ser candidates (importante para sesiones largas). user_features sin TTL pero invalidated por watch events; item_features actualizado incremental por engagement.

Eventos al stream con dedup. Eventos llevan event_id único; consumidores deduplican antes de actualizar features. Esto evita double-counting cuando hay retries de cliente.

Frase clave: "El schema reconoce que el feed es un join entre user state, item state y model output. Cada uno vive en el storage que mejor lo sirve."

Decisiones técnicas

TemaDecisiónPor qué
Generación de candidatosMezcla: trending, follow graph, embeddings ANN.Diversidad + relevancia, evita pozo informativo.
RankingModelo online con features pre-calculadas.Sub-100ms p99 sin entrenar en el path.
EventosStream analítico que actualiza features y métricas.Feedback rápido al modelo.
CursorToken opaco con sesión + offset.Resilient a re-rank y fácil de debuggear.
DiversidadReglas post-rank: no repetir creator, mezcla de topics.UX agradable y reducción de fatiga.
Cold startOnboarding guiado + popularidad regional.Sin historial, popularidad gana al modelo.

Cómo defiendo estas decisiones técnicas

Constraint: feed p99 < 200 ms con 600K QPS. No puedo entrenar online; no puedo hacer DB joins en path crítico; no puedo hacer model serving sin batching. Cada decisión sale de este budget.

Two-stage retrieval. Recall barato (~500 candidates en 30 ms) + rank fino (~100 ms). Si rankearas 100M items con DLRM, irías a horas por request. Recall + rank es la única forma de escalar.

Cursor opaco para resilience. Si me deployan un ranker nuevo entre dos requests, el cursor opaco no se rompe (el server lo reinterpreta). Si fuera offset, todos los users verían duplicados o gaps tras deploy.

Métricas que validan. Feed p99 (target < 200 ms). Completion rate (% de videos vistos > 50%). Diversity score (cuántos creators distintos en N items). Cold start retention D1 (target similar a D1 de existing users).

Cuándo cambio de opinión. Long-form content → cambio scoring de "completion rate" a "watch time normalized". Live streams en feed → agrego pipeline de signals en vivo. Multi-region active-active → particiono feature store por geo y serving regional.

Frase clave: "Cada decisión es un trade entre relevancia, diversidad y latencia. Mi modelo es solo uno de muchos signals; las business rules y diversity filters son tan importantes como el ranker."

Pitfalls

Modelo en path crítico

Entrenar online te dispara latencia. Mantén el modelo offline y sirve features.

Feedback lag

Si los eventos llegan tarde, el ranker no aprende rápido. Usa stream con baja latencia.

Eco chamber

Si sólo rankeas relevancia, el feed se aburre. Inyecta exploración.

Script de respuesta: “Separo el feed en candidate generation, ranking y post-procesamiento de diversidad. Los candidatos vienen de trending, follow-graph y vecinos cercanos en embeddings. El ranker es un modelo online sirviendo desde un feature store; los eventos van por stream para alimentar features y métricas. El cursor es opaco para sobrevivir a cambios de ranking. Mido p95, completion rate y tasa de exploración.”

Tiers de almacenamiento

El feed depende de un feature store sub-30 ms; los videos viejos pueden bajarse a tiers baratos sin afectar UX.

Hot · in-memory + CDN
Feature store online (user/item features), recent_seen set, ANN index en RAM, videos trending en CDN POPs.
Bigtable + Redis + CDN
~$0.25/GB/h Bigtable · ~$36/GB Redis
Warm · object storage Standard
Videos uploads último mes, embeddings actuales del modelo, eventos warehouse hot partition.
S3 Standard + BigQuery active partitions
~$0.023/GB/mes
Cold · object storage IA
Videos > 30 días con bajo engagement, embeddings de versiones de modelo viejas, training data archivado.
S3 Standard-IA / BigQuery long-term
~$0.0125/GB/mes
Frozen · archive
Videos > 1 año sin views, raw event archive (auditoría, reentrenamiento histórico).
S3 Glacier / Glacier Deep Archive
~$0.001–0.004/GB/mes

Cómo defenderlo en la entrevista

Pareto en videos. El 99% del watch time está en el 1% top de videos (siempre fresh / trending). Mantener el long-tail en S3 Standard es desperdicio. Lifecycle a IA tras 30 días sin views → ahorro masivo.

Embeddings versiones viejas en cold. Cada release del modelo regenera embeddings. Las versiones N-2 quedan disponibles pero raras (rollback, A/B). Cold storage hasta que se necesiten.

Frase clave: "Feature store en RAM porque path crítico, videos hot en CDN porque latencia, long-tail en IA porque acceso bajo demanda OK con +100ms, archive raw events en Glacier porque solo training histórico ocasional."

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

TikTok prioriza latencia brutal. LB es el default; Gateway solo para integraciones externas.

flowchart LR Client(["App / Móvil"]) --> LB LB{{"⚖️ L7 LOAD BALANCER
NGINX / ALB
region routing"}} --> Pod["Feed API Pods
+ JWT verifier (local)
+ Rate limit Redis"] Pod --> CG["Candidate Gen"] CG --> ANN Pod --> Rank["Online Ranker"] Rank --> FS Pod --> Diverse["Diversity Filter"] Client -. eventos .-> Stream Stream --> FS Client -- video bytes --> CDN(["CDN POPs"]) subgraph ANN["🧠 Embeddings ANN · HNSW / FAISS"] direction TB v1["video_vectors"] v2["user_vectors"] end subgraph FS["💾 Feature Store · Bigtable / Redis"] direction TB f1["user_features:id"] f2["item_features:id"] f3["recent_seen:id"] end subgraph Stream["📨 Kafka · watch/like/share"] direction TB k1["watch_events"] k2["like_events"] k3["share_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:#581c87,stroke:#f472b6,stroke-width:2px,color:#fce7f3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class CDN edge class Pod,CG,Rank,Diverse service class Client client

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

TikTok aprieta latencia hasta el límite. Cada milisegundo en el feed pesa: 600K QPS × 5 ms extra = recursos de cómputo enormes. El JWT lo verifico local en pod (con public key cacheada, sin llamar a auth service); rate limit con Redis (sliding window log). Gateway agregaría latencia y no aporta valor.

Frase clave: "Para feed serving uso LB con auth en pod — la latencia es product feature. Para Creator API y Open Platform (third-party apps) sí uso Gateway porque necesito quotas y observability por developer key."

Cuándo elegir API Gateway

  • Open Platform / Developer API con quotas y plans.
  • Brand integrations con rate limits especiales.
  • Multiple consumer types con políticas distintas.
  • OAuth complejo con scopes granulares (third-party login).

Cuándo elegir Load Balancer

  • Feed serving: 600K QPS, p99 < 200 ms — gateway agrega latencia inaceptable.
  • Eventos ingest (watch/like/share): masivo, fire-and-forget.
  • Auth simple: JWT con verifier local (public key cached).
  • App propia: no hay third-party que justifique gateway features.
  • Service mesh ya cubre auth/observability con Envoy sidecar.
  • Cost: a 600K QPS, gateway-per-request es prohibitivo.