TL;DR

1. The problem: why does Instagram need data pipelines and ML?

Everything you have seen so far in this guide — uploads, feeds, likes, messages — is about serving users right now. But a product with billions of users also needs to learn from what those users do:

The live databases can't answer these. The User DB and the Cassandra post store fetch one user's rows in a millisecond; "average session length per country over 90 days" would scan every row for hours and slow the app for everyone. So Instagram builds a parallel world: an analytics and ML platform fed by a copy of everything that happens. In the main diagram this is the Analytics & ML Training consumer on Kafka, whose trained ranking models flow back into the News Feed Service and Explore.

🧠 Analogy Think of a large supermarket chain. The checkout tills (OLTP) must process each customer's basket instantly and correctly. Every receipt is also copied to head office (the data warehouse), where analysts look at millions of receipts to answer "which products sell together?" and "did the new store layout increase sales?" (A/B tests). Finally, the head office uses those patterns to decide what to put at the end of each aisle for each store (ranking/recommendation). The tills never wait for head office — the analysis happens on a copy, slightly later.
📱 App events Service events DB snapshots/ CDC Kafkaevent bus Stream processingFlink: seconds Data lakeParquet on S3/HDFS Batch (Spark)hours Feature storeonline + offline Feed/Exploreonline inference ML trainingGPU cluster DashboardsA/B results models
The analytics & ML world: events flow through Kafka into a stream path (seconds) and a lake/batch path (hours). Both feed features, dashboards and model training; trained models are served back to Feed and Explore.

2. Event logging: the raw material

2.1 What is an event?

An event is an immutable record that "something happened at time T". A database row holds current state ("post 42 has 1,203 likes"); events hold history ("user 7 liked post 42 at 10:03:11"). History can rebuild state, never the reverse — which is why analytics systems love events.

{
  "event_id":    "8b1f0c2e-6a1d-4c3e-9e7a-2f6b1d9c0a11",   // UUID → lets consumers de-duplicate
  "event_name":  "feed_media_impression",
  "event_time":  "2026-10-23T09:12:44.318Z",              // when it happened on the device
  "server_time": "2026-10-23T09:12:47.902Z",              // when the server received it
  "user_id":     184467440737,
  "device":      {"os": "android", "app_version": "312.0.0", "country": "IN"},
  "session_id":  "s-55f1…",
  "props":       {"media_id": 3192…, "author_id": 9921…, "position": 4,
                  "surface": "home_feed", "view_ms": 2350, "experiment_ids": [881, 1204]}
}

2.2 Client events vs service events

Client (app/web) eventsServer / service events
ExamplesImpressions ("post was on screen for 2.3 s"), scrolls, taps, video watch time, app start time, crashes.Post created, like written, follow added, login, API latency, ranking decisions ("these 500 candidates were scored").
Only knowable where?Only the device knows what was actually seen and for how long.Only the server knows authoritative outcomes (did the write succeed?).
ReliabilityLossy: phones go offline, apps get killed, clocks are wrong, events can be duplicated on retry.Reliable: produced by our own code in the data center, published to Kafka.
TrustUntrusted — can be forged by bots. Never use for billing or security decisions without validation.Trusted.

2.3 How client events travel

📱 Appbuffer on disk
→batch every ~30 s, gzip
Logging endpointbehind LB / gateway
→validate schema
Kafkatopic per event family
→consumers
Data lakehourly partitions

Rough volume (assumption): 500M daily users × ~1,000 events each (impressions are the bulk) = 500 billion events/day. At ~200 bytes each after compression that's ~100 TB/day of raw logs, or ~1.2 GB/s on average. This is why the transport is Kafka-style partitioned logs and the storage is cheap object storage, not a relational database.

💡 Change data capture (CDC) Not every analytical table comes from events. To get the state of the User DB into the warehouse, you either take nightly snapshots (dump each shard, load it) or use CDC: tail the database's replication log (PostgreSQL WAL, MySQL binlog) and publish each row change to Kafka (e.g. with Debezium). CDC gives near-real-time copies without querying the production database.

3. OLTP vs OLAP and columnar storage

3.1 Two very different workloads

OLTP — Online Transaction ProcessingOLAP — Online Analytical Processing
Typical querySELECT * FROM users WHERE id = 42; insert one like.SELECT country, AVG(session_sec) FROM sessions WHERE day >= … GROUP BY country
Rows touched1 – 100Millions to trillions
Columns touchedAll columns of a few rowsA few columns of all rows
Latency targetMillisecondsSeconds to minutes (hours for big batch jobs)
Storage layoutRow-oriented (B-tree / LSM), current state, frequent updatesColumn-oriented, compressed, append-only history
Instagram examplesUser DB (PostgreSQL), post/comment store (Cassandra), RedisHive/Presto tables over the data lake, Spark jobs, real-time OLAP (Druid/Pinot-like)

Running analytics on OLTP databases is a classic mistake: one analyst's full-table scan evicts the hot working set from the buffer cache and makes the live app slow. Keep them separate and copy data across (events, CDC, snapshots). See Databases and Indexing & Storage Engines for the OLTP side.

3.2 Row vs column layout

Imagine a impressions table with columns (user_id, media_id, country, view_ms, ts). A row store keeps each row's fields together on disk; a column store keeps each column's values together:

ROW-ORIENTED (good for "give me row 2")
  [u1, m9, IN, 2350, t1] [u2, m4, BR, 800, t2] [u3, m9, IN, 4100, t3] ...

COLUMN-ORIENTED (good for "average view_ms by country")
  user_id : u1   u2   u3   u4   ...
  media_id: m9   m4   m9   m7   ...
  country : IN   BR   IN   IN   ...   → dictionary + run-length: {IN:0,BR:1} 0 1 0 0 ...
  view_ms : 2350 800  4100 1200 ...   → delta / bit-packed integers
  ts      : t1   t2   t3   t4   ...

Why columns win for analytics:

  1. Read only what you need. 2 of 5 columns here; real tables have 100+ columns, so savings are often 95%+.
  2. Dramatic compression. Similar, repeated values compress 5–20× with dictionary, run-length and bit-packing encodings.
  3. Skip whole blocks. Per-block min/max statistics let day = '2026-10-22' skip non-matching blocks.
  4. Vectorised execution. SIMD over tight arrays of one type is far faster than walking mixed rows.

Worked example (assumption): 1 trillion impression rows × 120 columns × ~8 bytes ≈ 960 TB raw. A query needs 3 columns → 24 TB; with 8× compression → 3 TB; with partition pruning to one day of 90 retained → ~33 GB actually read. Spread across 1,000 workers that's 33 MB each — seconds, not days. The cost: writing or updating a single row is awkward (you'd touch 120 column files), which is why columnar stores are append-heavy and batch-loaded.

4. Data lake vs data warehouse (and the lakehouse)

Data lakeData warehouse
What it isCheap object storage (S3/HDFS) holding files in open formats (Parquet, ORC, Avro, JSON) — raw and processed.A managed analytical database with its own optimised storage, SQL engine and catalog (Snowflake, BigQuery, Redshift).
SchemaSchema-on-read: store anything, interpret when querying.Schema-on-write: data must fit table definitions on load.
Data typesStructured, semi-structured, unstructured (images, model checkpoints).Mostly structured tables.
Cost / scaleLowest $/TB; effectively unlimited. Risk: a "data swamp" without catalog and ownership.Higher $/TB; compute often billed per query.

The modern compromise is the lakehouse: Parquet on object storage plus a table format (Apache Iceberg, Delta Lake, Hudi) adding ACID commits, schema evolution, time travel and row-level deletes (essential for GDPR deletion), readable by Spark, Trino and Flink alike.

Partitioning is the key lake design choice: /impressions/ds=2026-10-22/hour=09/part-0001.parquet lets date-filtered queries read only matching directories. Avoid too many tiny files (the "small files problem"); compaction jobs merge them. A common layering is raw/bronze (as received) → clean/silver (deduplicated, validated, bots filtered) → aggregated/gold (daily metrics, feature tables, experiment results).

5. Batch processing

5.1 MapReduce intuition

Problem: count likes per post for yesterday from 20 TB of like events. One machine reading at 200 MB/s would take ~28 hours. Split the work across 1,000 machines and it takes under 2 minutes of reading — but you must combine partial counts correctly. MapReduce (Google, 2004; open-sourced as Hadoop) gave a simple recipe:

  1. Map: each worker reads a chunk of input and emits (key, value) pairs — here (post_id, 1) per like.
  2. Shuffle: the framework routes all pairs with the same key to the same reducer, by hash(key) mod R — the same idea as hash sharding. This network-heavy step is usually the bottleneck.
  3. Reduce: each reducer sums the values for its keys and writes output files.
map(like_event):
    emit(like_event.post_id, 1)

combine(post_id, counts):          # optional local pre-aggregation on the mapper
    emit(post_id, sum(counts))     # sends (p42, 917) instead of 917 × (p42, 1)

reduce(post_id, counts):
    emit(post_id, sum(counts))

Workers are stateless and inputs immutable, so failed tasks are simply re-run elsewhere — free fault tolerance. The weakness is skew: a celebrity post with 30M likes sends 30M pairs to one reducer, the straggler for the whole job. Fixes: combiners, "salting" hot keys (p42#0…p42#9, then re-aggregate), broadcast joins.

5.2 The modern batch engines

EngineIdeaWhen to use
Hadoop MapReduceMap → shuffle → reduce, writing intermediates to disk between stages.Historic; learn it for intuition.
Apache HiveSQL compiled into batch jobs over HDFS/S3 files; introduced the metastore catalog. Created at Facebook.Scheduled SQL ETL; its metastore is still the standard catalog.
Apache SparkIn-memory DAG of transformations; DataFrame/SQL API; MLlib.The default for big ETL and ML data prep.
Presto / TrinoPipelined in-memory MPP SQL, no intermediate disk writes. Created at Facebook for interactive queries.Ad-hoc analysis over the lake in seconds–minutes.

A typical daily pipeline is a DAG run by a scheduler such as Airflow: wait for yesterday's partitions → clean → join dimensions → compute metrics, experiment results, training sets. Each step is idempotent (re-running ds=2026-10-22 overwrites that partition), making backfills after bug fixes safe.

6. Stream processing

Batch answers arrive hours later. Some questions can't wait: "Is this post going viral?" (pre-warm the CDN), "Has this account liked 500 posts in 2 minutes?" (spam), trending hashtags for Search, or fresh interest features for the next feed refresh. Stream processors consume Kafka topics continuously and update results incrementally.

6.1 Engines

6.2 Event time vs processing time

Event time = when it happened on the device; processing time = when the processor sees it. Counting likes per minute by processing time, a 5-minute consumer lag looks like likes dropping to zero then spiking — a lie. Group by event time — but how long do you wait for stragglers?

6.3 Windows

time ─────────────────────────────────────────────────────────────►
events:   x  x   x x     x        x  x x x            x     x

TUMBLING (fixed, non-overlapping, e.g. 1 min)
          |── w1 ──|── w2 ──|── w3 ──|── w4 ──|── w5 ──|

SLIDING / HOPPING (size 2 min, slide 1 min → overlapping)
          |──── w1 ────|
                   |──── w2 ────|
                            |──── w3 ────|

SESSION (gap-based: window closes after 30 s of inactivity per user)
          |─ s1 ──────|       |─ s2 ───────|        |─ s3 ──────|

6.4 Watermarks and late data

A watermark is the stream processor's declaration: "I believe I have now seen all events with event time ≤ W." It typically trails the maximum observed event time by an allowed lateness, e.g. W = max_event_time_seen − 2 minutes. When the watermark passes the end of a window, the window is closed and its result emitted.

 window [09:00, 09:01)        watermark = max event time − 2 min
 ─────────────────────────────────────────────────────────────────
 09:00:10 event (on time)          → added to window
 09:02:30 max seen → W = 09:00:30  → window still open
 09:03:05 max seen → W = 09:01:05  → W ≥ 09:01 → EMIT count, close window
 09:03:20 a 09:00:40 event arrives → LATE: drop, send to side output,
                                     or update result if "allowed lateness"

The trade-off is explicit: a larger watermark delay gives more complete results but higher latency; a smaller one gives fast but less complete results. Very late events (hours) are typically left to the batch layer, which recomputes exact numbers later.

6.5 Exactly-once (really "effectively once")

Kafka delivers at-least-once. Flink achieves exactly-once state by checkpointing operator state together with the matching Kafka offsets and rewinding both on failure. External outputs also need transactional sinks or idempotent writes (SET count[post][minute] = 917, not INCR) — see Resilience.

7. Lambda vs Kappa architectures

LAMBDA KAPPA Kafka events Batch layerSpark over full history Speed layerFlink, recent data only Batch viewsexact, hours old Real-time viewsapprox., seconds old Serving layermerge batch + real-time Kafka loglong retention Stream job v1one code path v2 replays from offset 0 Serving table Queriesswitch to v2 when caught up
Lambda runs two pipelines (batch for correctness, speed for freshness) and merges them. Kappa runs a single streaming pipeline and handles reprocessing by replaying the retained log.
LambdaKappa
PipelinesTwo: batch + streaming, merged at query time.One: streaming; reprocess by replaying the log into a new job version.
StrengthBatch is simple, exact, can recompute anything.One codebase and semantics; no drift.
WeaknessSame logic written twice (Spark + Flink) — they diverge.Needs long log retention and a stream engine strong enough for huge replays.

Most large companies run hybrids: streaming for fresh features and counters, batch for canonical daily metrics, training and backfills.

8. ETL vs ELT

ETL — Extract, Transform, LoadELT — Extract, Load, Transform
OrderTransform on a separate tier, then load clean data.Load raw data into the lake/warehouse, then transform there with SQL (e.g. dbt).
Raw data kept?Often not — a buggy transform may lose the original.Yes — re-transform history any time.
Good whenSensitive fields must be removed before landing, or storage is expensive.Storage is cheap and compute elastic (the modern default).

Instagram-like systems mostly do ELT into the lake — but drop or pseudonymise unneeded personal fields at ingestion (a little "T" before the "L").

9. Product metrics: measuring health

9.1 Active users and stickiness

Exact distinct counts come from daily batch jobs; real-time dashboards use HyperLogLog sketches (~12 KB each, ~1–2% error, mergeable across days).

9.2 Retention and cohorts

Retention asks "of the users who signed up (or were active) in period X, what fraction is still active N days later?" Group users into cohorts by signup week and track each cohort:

Signup cohort (assumed data)UsersD1D7D30
Week of 1 Sep2.0M58%41%30%
Week of 8 Sep2.1M57%40%29%
Week of 15 Sep (new onboarding)2.2M63%46%34%

A flattening curve (D30 ≈ D60 ≈ D90) means the product has found users who stay. Cohorts prevent a classic trap: total DAU can grow (marketing brings new users) while every cohort retains worse than the last. Other common metrics: time spent, sessions per user, % of users who post (the supply side of a social network), and crash-free sessions (see Observability).

Teams usually pick a north-star metric that captures long-term value (e.g. "meaningful daily engagement") plus guardrail metrics that must not get worse (crash rate, reports of bad content, latency, unfollows). A change that increases time spent by showing more outrage-bait but also increases "hide"/"report" rates fails its guardrails.

10. A/B testing and experimentation

An A/B test (online controlled experiment) randomly splits users into a control group (current product, A) and one or more treatment groups (B). Because assignment is random, the only systematic difference between groups is the change — so a metric difference beyond random noise is caused by the change. This is how you answer "did the bigger like button help?" without guessing.

10.1 Assigning users

def assign(user_id, experiment):
    # deterministic: same user always lands in the same bucket, no DB lookup needed
    bucket = murmurhash(f"{experiment.salt}:{user_id}") % 1000      # 0..999
    if bucket < experiment.control_buckets:   return "control"      # e.g. 0..49   = 5%
    if bucket < experiment.treatment_buckets: return "treatment"    # e.g. 50..99  = 5%
    return "not_in_experiment"

10.2 Reading the result: significance, power, sample size

Suppose control users comment on 10.0% of days and treatment users on 10.1%. Is +1% relative real, or noise? Statistics answers with a p-value: the probability of seeing a difference at least this big if the change truly did nothing. By convention p < 0.05 is "statistically significant". Power (often 80%) is the probability you detect a real effect of a given size.

A handy rule of thumb for 80% power at α = 0.05 is n per group ≈ 16 × σ² / δ², where σ² is the metric's variance and δ the absolute effect you want to detect. For a yes/no metric with p = 0.10, σ² = p(1−p) = 0.09:

Detect δ = 0.001 (10.0% → 10.1%, a 1% relative lift):
  n ≈ 16 × 0.09 / 0.001² = 1,440,000 users per group
Detect δ = 0.0001 (0.1% relative lift):
  n ≈ 144,000,000 users per group   ← 100× more, because n ∝ 1/δ²

That 1/δ² is why Instagram-scale companies can measure tiny improvements that a startup never could — and why even they need variance-reduction techniques like CUPED (using each user's pre-experiment behaviour to cancel out noise).

10.3 Experimentation pitfalls

11. ML ranking: the multi-stage pipeline

For each feed request, the News Feed Service has a candidate pool of perhaps a few thousand unseen posts from accounts the user follows (plus, today, suggested posts). For Explore and Reels, the pool is effectively all public content — billions of items. Running a large neural network that takes ~1 ms of compute per item over billions of items for every request is impossible. The solution is a funnel: cheap methods shrink the pool, expensive methods order the survivors.

1. Candidate generation (retrieval)followed accounts' posts, ANN on embeddings, trending, graph walks 2. Lightweight ranking (first pass)small model / distilled model, few features 3. Heavy rankingbig multi-task neural net, 1000s of features 4. Re-ranking & rulesdiversity, integrity, ads, seen billions → ~1,000scost per item: ~nanoseconds ~1,000s → ~500cost per item: ~microseconds ~500 → ~50–100 scoredcost per item: ~0.1–1 ms of compute → final ordered page (~10–25)cheap logic over a short list
The ranking funnel. Each stage is more expensive per item but sees far fewer items, keeping the total cost and latency bounded. (Numbers are illustrative assumptions.)

11.1 Stage 1 — Candidate generation (retrieval)

Goal: high recall cheaply — don't miss good items; precision comes later. Typically several sources run in parallel and their results are unioned:

11.2 Stage 2 — Lightweight ranking

A small model (logistic regression, a small gradient-boosted tree, or a distilled neural net trained to mimic the heavy model) scores thousands of candidates using a handful of cheap, precomputed features and keeps the top few hundred. Its job is to be roughly right, very fast.

11.3 Stage 3 — Heavy ranking

A large deep neural network with thousands of features — often a multi-task model that predicts several probabilities at once for each (user, post) pair:

P(like), P(comment), P(share), P(save), P(tap profile), E[watch time], P(follow author),
P(hide / "not interested"), P(report)

These predictions are combined by a value model — a weighted formula that encodes what the product wants to optimise:

score = w1·P(like) + w2·P(comment) + w3·P(share) + w4·P(save)
      + w5·E[watch_time] − w6·P(hide) − w7·P(report)

The weights are a product decision tuned by A/B tests. Predicting components separately makes the system debuggable and lets you change goals (value comments and shares over passive likes) without retraining.

11.4 Stage 4 — Re-ranking for diversity, integrity and business rules

Sorting purely by score produces bad pages — five carousels from the same meme account in a row, or three near-duplicate posts about the same news. The final stage applies list-level logic:

11.5 Features: what the models look at

A feature is a numeric input to a model. Ranking quality comes far more from good features than from clever model architectures.

🤝 Affinity (user × author)

How often and how recently you like, comment on, DM or view this author; close friends; mutual follows. Usually the strongest feed signal.

🖼️ Content / post

Media type, age, engagement so far (likes per impression), content embeddings, topic, quality flags.

👤 Viewer

Your preferences (finish videos? like carousels?), interest embeddings, device, network, country.

📍 Context

Time of day, surface (feed vs Explore), position in session, what you just saw.

🎯 Predictions as features

Outputs of other models, e.g. the light model's engagement probability or a spam score.

Freshness varies: "likes in the last 5 minutes" needs stream jobs; "90-day watch completion" can be daily batch. Feature computation is often the most expensive part of the system.

11.6 The feature store

A feature store computes, stores and serves features consistently for training and serving: an offline store (full timestamped history in the lake, for training sets) and an online store (latest value per user/post/user×author in a low-latency KV store such as Redis or Cassandra, read in a few ms). It prevents two notorious bugs:

  1. Training–serving skew: training computes "likes in last 7 days" in Spark SQL, serving computes it in C++ slightly differently (time zones, dedup, defaults) — the model quietly underperforms. Define each feature once and materialise it to both stores.
  2. Label leakage: a training example "did the user like post P shown at 10:00?" must use feature values as of 10:00. Using today's "likes on this author" includes the very like being predicted — brilliant offline, useless online. Feature stores do point-in-time joins.

11.7 Offline training vs online inference

Offline trainingOnline inference (serving)
WhereGPU/accelerator training clusters reading the lake.Ranking service fleets (CPU or GPU) next to the Feed/Explore services.
InputLogged impressions joined with outcomes (labels: liked? watched 80%?) and point-in-time features. Billions of examples.One request: a user and ~500 candidates, features fetched from the online store.
LatencyHours to days per training run.Tens of milliseconds per request.
CadenceRetrain daily/hourly, or continuously fine-tune (online learning) so the model keeps up with new trends and content.Every feed load, 100,000s of requests/s.
ValidationOffline metrics on held-out data (AUC, log-loss, NDCG).Shadow mode → small A/B test → gradual rollout; watch online metrics and guardrails.

The release flow: train → evaluate offline → model registry → shadow scoring (output not shown; compare distributions) → 1% A/B test → ramp up. Offline gains often don't translate online, so the A/B test is the final judge.

11.8 Latency budgets

The user expects the feed in a few hundred ms end-to-end, so ranking might get ~100–200 ms of server time. An illustrative budget (assumption):

StepBudget (p99)How it stays within budget
Candidate generation (parallel sources)~30 msPrecomputed timelines in Redis; ANN indexes in memory; sources run concurrently with per-source timeouts (a slow source is simply skipped).
Feature fetch~20 msBatched multi-get from online store; per-request caches; precomputed user features fetched once.
Lightweight ranking~10 msTiny model, vectorised scoring of thousands of items.
Heavy ranking~50 msBatch all candidates into one model call; split across workers; GPUs/accelerators; quantised models.
Re-ranking & hydration~15 msSimple rules over a short list; fetch post metadata and signed CDN URLs.

When ranking is overloaded or down, the feed must still work: fall back to a cheaper model or to the chronological/cached timeline. That is covered in Resilience patterns (graceful degradation).

11.9 Feedback loops

Ranking models learn from data the ranker itself produced: items ranked high get more impressions, so more likes, so rank even higher — "we showed it a lot" looks like "people love it". Remedies:

12. Explore recommendations: embeddings, two towers, ANN

Feed ranks posts from accounts you chose. Explore (and Reels) must discover content from accounts you've never heard of — retrieval over billions of items. The core tool is the embedding.

12.1 Embeddings

An embedding is a vector (e.g. 128 floats) representing an entity so that similar entities get nearby vectors (by dot product or cosine). It's learned from behaviour: if users who engage with account A also engage with B, training pulls A and B together — word2vec's idea applied to "accounts appearing in the same sessions".

         ▲ dim 2                      (2-D toy; real embeddings have ~128+ dims)
         │     • latte art
         │   • coffee shop  • barista reels          ← "coffee" region
         │
         │                       • road bike
         │                    • cycling tours        ← "cycling" region
         │  ★ you
         │
         └──────────────────────────────────────► dim 1
  Your user vector ★ lies near coffee content → nearest neighbours = coffee posts

12.2 Two-tower models

User featureshistory, follows, country, recent views User towerneural net layers u (128 floats) Item featuresauthor, image/text embedding, topic Item towerneural net layers v (128 floats) score = u · vdot product ANN indexall item vectors v precomputed offline
A two-tower model: user and item features are encoded separately, and relevance is just a dot product. Item vectors can therefore be precomputed and indexed; at request time only the user tower runs, followed by a nearest-neighbour lookup.

Training uses positive pairs (user, item they engaged with) and negatives (random or other in-batch items); the loss pushes u·v up for positives and down for negatives. The key property: the towers don't interact until the dot product. That costs some accuracy versus a heavy ranker, but makes retrieval over billions of items feasible — item vectors are computed offline, and per request you compute one user vector and look up the highest dot products.

12.3 Approximate nearest-neighbour (ANN) search

Brute force is too slow: 1 billion items × 128 dims = 128 billion multiply-adds per request. ANN algorithms trade a little recall (finding ~95% of the true top-100) for 100–1000× speed:

Libraries include FAISS (open-sourced by Meta), ScaNN and hnswlib. Indexes are rebuilt periodically, updated incrementally as new posts arrive, and sharded across machines — the query fans out to every shard and results are merged.

12.4 Cold start

Collaborative signals need history. Three cold-start cases:

CaseProblemTechniques
New userNo engagement history.Onboarding (interests, contacts, suggested accounts); context (country, language); popular high-quality content; learn fast from the first session; explore more early.
New post"Engagement so far" features are zero.Content embeddings and the author's track record; a small exploration budget of impressions; Bayesian smoothing of early rates.
New creatorNo followers → never surfaced → never gains followers.Reserved exploration traffic; content-based retrieval; boosts for original content.

12.5 Filter bubbles and integrity

A recommender that only exploits learned preferences narrows over time — three fitness reels lead to only fitness reels — a filter bubble. Worse, engagement optimisation can amplify sensational or harmful content. Mitigations:

13. How Instagram uses analytics & ML

📱 Appimpressions, taps, watch time
→batched logs
Kafkaevents + domain events
→
Lake / warehouseHive-style tables
→training data
Model trainingPyTorch on GPUs
→model artifacts
Feed / Explore rankingonline inference
  1. Logging. The apps log impressions (which post was on screen, for how long), interactions and performance telemetry; services log domain events (the same post created / like / follow events that drive Kafka consumers in the main diagram) and ranking decisions.
  2. Lake & warehouse. Events land in partitioned, columnar tables. Facebook created Hive and Presto for this workload and has publicly described Scribe (log transport) and Scuba (real-time analytics); Instagram, inside Meta's infrastructure since its migration (see Multi-Region), is described as using this shared platform.
  3. Metrics & experiments. Daily pipelines compute DAU/MAU, retention and creator metrics; an experimentation platform evaluates every launch.
  4. Ranking. Feed became ranked (not chronological) in 2016. Instagram's leadership has publicly explained that Feed, Stories, Explore and Reels each have their own ranking system predicting actions such as time spent, like, comment, share/save and profile tap, combined into a score with integrity rules applied. Chronological "Following" and "Favorites" views were added in 2022.
  5. Explore. Instagram's engineering blog (2019) publicly described Explore as multi-stage: account embeddings learned from interaction sequences (ig2vec), candidate retrieval, then a few hundred candidates funnelled through a distilled lightweight model and heavier neural networks — the funnel of section 11.
📸 In Instagram (publicly described + assumptions) A feed request in the main diagram: the News Feed Service reads candidate post IDs from the precomputed Redis timeline, fetches online features, calls ranking, applies diversity/integrity rules, and hydrates the page with metadata and signed CDN URLs. Every logged impression becomes tomorrow's training data. Meta has open-sourced PyTorch, FAISS and DLRM (a reference recommendation-model architecture), which show the general shape of these systems; exact model sizes and weights are not public.

Rough numbers (assumptions)

Feed requests:   500M DAU × ~20 feed loads/day ≈ 10B requests/day ≈ 115k/s avg (~300k/s peak)
Heavy scoring:   ~300 candidates per request → ~35M model evaluations/s on average
                 → this is why stages 1–2 exist, and why scoring runs batched on accelerators
Training data:   500B impression events/day; even a 1% sample = 5B labelled examples/day
Event storage:   ~100 TB/day compressed raw logs → ~36 PB/year before retention/aggregation

14. Trade-offs & common pitfalls

✅ Benefits

  • Decisions backed by controlled experiments, not opinions
  • Ranking surfaces the posts people care about first
  • OLAP answers huge questions without harming the live app
  • Immutable logs let you recompute anything after a bug fix

⚠️ Costs

  • Huge storage and compute bills (petabytes, GPU fleets)
  • Complexity: two pipelines, feature stores, model registries
  • Data-quality issues silently corrupt metrics and models
  • Opaque ranking; feedback loops and filter bubbles
  • Privacy burden: deletion, consent, retention

Common pitfalls

15. Real-world technologies

TechnologyCategoryWhen to use
Kafka + Schema Registry, DebeziumEvents, CDCReplayable event streams; DB changes out of OLTP stores.
S3 / HDFS + Parquet; Iceberg / Delta / HudiLake storage, table formatsCheap columnar storage; ACID, time travel and row-level deletes.
Snowflake, BigQuery, RedshiftCloud warehousesManaged SQL analytics.
Spark, Hive; Presto / TrinoBatch; interactive SQLLarge ETL and training data; ad-hoc queries over the lake.
Flink, Kafka StreamsStream processingWindows, fresh features, spam signals.
Druid, Pinot, ClickHouseReal-time OLAPSub-second dashboards on fresh events.
Airflow; Feast / TectonOrchestration; feature storeScheduling batch DAGs and backfills; consistent online/offline features.
PyTorch, XGBoost; FAISS, ScaNN, MilvusTraining; vector searchDeep and tree-based ranking models; ANN retrieval over billions of items.

16. Interview corner

Design the pipeline that computes "likes per post per minute" for a real-time creator dashboard.

Like Service publishes like_created events (event_id, post_id, event_time) to Kafka partitioned by post_id. A Flink job keys by post, uses 1-minute tumbling event-time windows with watermark = max event time − 30 s, and de-duplicates by event_id. On window close it upserts (post_id, minute) → count into Redis/Cassandra or Pinot — idempotent, so replays don't double count. Late events go to a side output and a nightly batch job recomputes exact counts (lambda-style). Hot celebrity posts are pre-aggregated per partition; unlikes are −1 events.

Why not just run analytics queries against the Cassandra and PostgreSQL clusters?

OLTP stores are tuned for key lookups and production latency. Analytical scans compete for disk/CPU and evict caches, slowing the live app; data is sharded by user_id so cross-shard joins are impractical; and they hold current state, not history. Replicate via events/CDC into a columnar lake where scans are cheap and compute scales independently.

Walk me through how Instagram's feed ranking works at a high level.

Multi-stage funnel. (1) Candidates: unseen posts from the Redis timeline, pulled celebrity posts, and recommended posts from ANN retrieval — a few thousand. (2) A light/distilled model narrows to a few hundred. (3) A heavy multi-task network predicts P(like), P(comment), P(share), P(save), expected time, P(hide) from online feature-store features (author affinity, engagement so far, content embeddings, context); a value model combines them. (4) Re-ranking applies diversity caps, integrity demotions, seen-state and ads. The page is hydrated with metadata and CDN URLs; impressions are logged for retraining; changes ship via A/B tests. If ranking is down, serve the cached chronological timeline.

How would you build retrieval for Explore over billions of posts?

Train a two-tower model on (user, engaged-item) pairs with in-batch negatives. Precompute item embeddings (new posts as soon as media processing finishes) into a sharded ANN index (IVF-PQ or HNSW via FAISS). Per request, run the user tower, query each shard for top-K by dot product, merge, union with trending and graph-based sources, filter for eligibility (recommendable, unseen, not blocked), and hand a few thousand candidates to the ranking stages. Monitor ANN recall against exact search on samples.

Lambda or Kappa — which would you choose and why?

Lambda gives exact batch recomputation but duplicates logic across two engines, which drift. Kappa has one code path and reprocesses by replaying the log, but needs long retention and a strong stream engine. For Instagram I'd run a hybrid: Flink for fresh features and real-time counters, batch for canonical daily metrics, training sets and backfills, with shared definitions (same SQL/engine) to avoid divergence.

How do you know a new ranking model is actually better?

Offline: evaluate on a time-split holdout (train days 1–28, test day 29) with AUC/log-loss and NDCG. Shadow-deploy to compare score distributions and latency. Then an A/B test with a pre-registered primary metric, guardrails (hides, reports, latency, creator metrics), a sample size computed for the minimum effect you care about, 1–2+ weeks to wash out novelty, and an SRM check. Ramp gradually; keep a long-term holdout.

17. Check your understanding

A table has 200 columns; a query reads 4 of them across one day out of 365 stored days. Roughly what fraction of the raw data does a partitioned columnar store read, before compression?

(4 / 200) × (1 / 365) ≈ 0.02 × 0.0027 ≈ 0.0055% — about 1/18,000 of the data. Compression then reduces it further. A row store without partitioning would read all of it. This is why OLAP uses columnar, partitioned storage.

DAU is 300M and MAU is 1B. What is stickiness and what does it mean?

300M / 1B = 30%. The average monthly user is active on about 9 of 30 days. Compared with a social app at 50–60%, this suggests many monthly users are casual; you'd look at retention cohorts to see whether new users are failing to form a habit.

Your Flink job counts likes per minute with watermark = max event time − 1 min. A phone that was offline for 10 minutes uploads its likes. What happens, and how do you get correct numbers?

Late data: their windows already closed, so they're dropped or sent to a side output — real-time numbers are slightly low. Correct numbers come from the batch layer recomputing from the lake (lambda), or from longer allowed lateness with result updates (more latency/state). A completeness vs latency trade-off.

Control commenting rate is 5%. How many users per group do you roughly need to detect an absolute lift of 0.1 percentage points (5.0% → 5.1%) with 80% power?

σ² = 0.05 × 0.95 ≈ 0.0475; δ = 0.001. n ≈ 16 × 0.0475 / 0.000001 ≈ 760,000 per group. Halving δ would need 4× as many users.

You assigned 50/50 but the experiment shows 10.2M users in control and 9.8M in treatment. Ship it if the metric is positive?

No. A 2% imbalance over 20M users is a sample ratio mismatch — far outside random chance. Likely the treatment crashes or fails to log for some users, so the remaining treatment users are a biased subset. Investigate and fix before trusting any metric.

A new model shows +3% AUC offline but the A/B test shows no improvement. Give three plausible reasons.

(1) Label leakage — offline features contained future information. (2) Training–serving skew — features computed differently in production. (3) Offline ≠ product metric — better like-prediction doesn't mean a better final page after the value model and re-ranking. Also: added latency, or an under-powered test.

🎯 Key takeaways

Related lessons