- Every tap, scroll, like and video view becomes an event. Events flow through Kafka into a data lake / warehouse, where they power dashboards, experiments and machine-learning models.
- OLTP databases (PostgreSQL, Cassandra) serve the live app one row at a time; OLAP systems (columnar warehouses) answer questions over billions of rows. They are different tools for different jobs.
- Batch processing (Spark, Hive, Presto) computes exact answers over huge historical data hours later; stream processing (Flink, Kafka Streams) computes approximate-to-exact answers seconds later using windows and watermarks. Lambda runs both; Kappa runs only streams.
- Product health is measured with DAU/MAU, retention cohorts and changes are proven with A/B tests, not opinions.
- Feed and Explore ranking is a multi-stage funnel: candidate generation → lightweight ranking → heavy ranking → re-ranking, using features from a feature store, models trained offline and served online inside a tight latency budget.
- Explore uses embeddings, two-tower models and approximate nearest-neighbour search; you must also handle cold start, feedback loops, filter bubbles and integrity.
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:
- "Is the product healthy?" How many people opened the app today? Are new users still around a month later?
- "Did our change help?" Does a bigger like button increase engagement or just annoy people? You need a controlled experiment on millions of users.
- "What should each person see?" Users follow hundreds of accounts that post far more than anyone can scroll, and Explore must pick a few dozen posts out of billions. Pure time order buries your best friend's wedding photo under 40 meme posts.
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.
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) events | Server / service events | |
|---|---|---|
| Examples | Impressions ("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?). |
| Reliability | Lossy: 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. |
| Trust | Untrusted — can be forged by bots. Never use for billing or security decisions without validation. | Trusted. |
2.3 How client events travel
- Batch and compress on the device. One HTTP request per scroll event would drain batteries and flood the load balancers; apps buffer events in a local file and upload batches.
- Persist before sending. Failed uploads stay on disk and are retried — so the pipeline is at-least-once, and
event_idlets downstream jobs drop duplicates. - Two timestamps. Analyse by
event_time, but device clocks can be wrong by hours;server_timelets you detect skew. Events can arrive days late (a phone offline on a flight) — see watermarks below. - Schemas. Every event type has a declared schema (Avro/Protobuf/Thrift) in a registry; malformed events are quarantined so one buggy app version can't poison every table. Evolution rule: only add optional fields, never change a field's meaning.
- Sampling. Ultra-high-volume, low-value telemetry may be sampled at 1% (multiply counts by 100); business-critical events are never sampled.
- Privacy at the source. Log IDs, not content; never log passwords or message bodies; tag fields by sensitivity for deletion/retention (see Security & Privacy).
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.
3. OLTP vs OLAP and columnar storage
3.1 Two very different workloads
| OLTP — Online Transaction Processing | OLAP — Online Analytical Processing | |
|---|---|---|
| Typical query | SELECT * FROM users WHERE id = 42; insert one like. | SELECT country, AVG(session_sec) FROM sessions WHERE day >= … GROUP BY country |
| Rows touched | 1 – 100 | Millions to trillions |
| Columns touched | All columns of a few rows | A few columns of all rows |
| Latency target | Milliseconds | Seconds to minutes (hours for big batch jobs) |
| Storage layout | Row-oriented (B-tree / LSM), current state, frequent updates | Column-oriented, compressed, append-only history |
| Instagram examples | User DB (PostgreSQL), post/comment store (Cassandra), Redis | Hive/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:
- Read only what you need. 2 of 5 columns here; real tables have 100+ columns, so savings are often 95%+.
- Dramatic compression. Similar, repeated values compress 5–20× with dictionary, run-length and bit-packing encodings.
- Skip whole blocks. Per-block min/max statistics let
day = '2026-10-22'skip non-matching blocks. - 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 lake | Data warehouse | |
|---|---|---|
| What it is | Cheap 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). |
| Schema | Schema-on-read: store anything, interpret when querying. | Schema-on-write: data must fit table definitions on load. |
| Data types | Structured, semi-structured, unstructured (images, model checkpoints). | Mostly structured tables. |
| Cost / scale | Lowest $/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:
- Map: each worker reads a chunk of input and emits
(key, value)pairs — here(post_id, 1)per like. - 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. - 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
| Engine | Idea | When to use |
|---|---|---|
| Hadoop MapReduce | Map → shuffle → reduce, writing intermediates to disk between stages. | Historic; learn it for intuition. |
| Apache Hive | SQL 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 Spark | In-memory DAG of transformations; DataFrame/SQL API; MLlib. | The default for big ETL and ML data prep. |
| Presto / Trino | Pipelined 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
- Apache Flink — distributed stream processor with large managed state (RocksDB), event-time windows, watermarks and exactly-once state via checkpoints. The usual choice for serious real-time pipelines.
- Kafka Streams — a Java library embedded in your service, state in local RocksDB backed by Kafka changelog topics. Simple when input and output are both Kafka.
- Spark Structured Streaming — micro-batches every few hundred ms to seconds with the Spark API.
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 ──────|
- Tumbling: "likes per post per minute" — each event belongs to exactly one window.
- Sliding/hopping: "likes in the last 10 minutes, updated every minute" — smooth trending scores.
- Session: "how long was each app session?" — windows are defined by gaps in a user's activity.
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 | |
|---|---|---|
| Pipelines | Two: batch + streaming, merged at query time. | One: streaming; reprocess by replaying the log into a new job version. |
| Strength | Batch is simple, exact, can recompute anything. | One codebase and semantics; no drift. |
| Weakness | Same 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, Load | ELT — Extract, Load, Transform | |
|---|---|---|
| Order | Transform 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 when | Sensitive 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
- DAU (daily active users): distinct users who did a precisely defined qualifying action on a day ("opened the app" vs "viewed a post" can differ by tens of millions).
- MAU (monthly active users): distinct users active at least once in the trailing 28/30 days.
- Stickiness = DAU / MAU. Assume DAU = 500M and MAU = 800M → 62.5%: the average monthly user shows up ~19 of 30 days. Social apps aim for 50%+.
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) | Users | D1 | D7 | D30 |
|---|---|---|---|---|
| Week of 1 Sep | 2.0M | 58% | 41% | 30% |
| Week of 8 Sep | 2.1M | 57% | 40% | 29% |
| Week of 15 Sep (new onboarding) | 2.2M | 63% | 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"
- Randomisation unit: usually the user (consistent experience across sessions), not the request.
- Per-experiment salt: experiments shuffle users independently, so many can run on overlapping users ("layers").
- Exposure logging: log when the user actually encounters the change (the
experiment_idsfield above) and join metrics to exposures.
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
- Peeking: stopping the moment p < 0.05 inflates false positives. Fix the duration up front (or use sequential tests).
- Multiple comparisons: test 40 metrics and ~2 will be "significant" by chance. Pre-register a primary metric.
- Novelty effects: users click a shiny new button for a week, then stop. Run 2+ weeks and check the trend.
- Sample ratio mismatch (SRM): configured 50/50, got 50.6/49.4 over millions of users → something (crash, logging bug) drops users from one arm; results are untrustworthy.
- Network effects: treatment users interact with control users (more comments sent → control receives more), shrinking the measured difference. Fix with cluster randomisation (friend-clusters or countries).
- Holdouts: keep ~1% of users without a quarter's launches to measure the cumulative long-term effect.
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.
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:
- Graph-based: recent posts from accounts you follow (read from your precomputed timeline in the Redis feed cache, plus pulled celebrity posts — see fan-out); posts liked by accounts similar to ones you engage with.
- Embedding-based: nearest neighbours of your user embedding in item-embedding space (section 12).
- Heuristic: trending in your country, popular in topics you like, fresh posts needing exposure (cold start).
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:
- Diversity: no more than N consecutive items from one author or one topic; mix photos, carousels and videos; techniques include MMR (maximal marginal relevance) — penalise items too similar to those already chosen.
- Integrity: demote or remove content that classifiers flag as borderline (misinformation, spam, clickbait), content from accounts under enforcement, or items the user has already seen.
- Business rules: insert ads at allowed slots (with their own auction), boost "close friends" or new posts from accounts you frequently interact with, respect user controls (muted accounts, "not interested", sensitive-content settings).
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:
- 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.
- 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 training | Online inference (serving) | |
|---|---|---|
| Where | GPU/accelerator training clusters reading the lake. | Ranking service fleets (CPU or GPU) next to the Feed/Explore services. |
| Input | Logged 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. |
| Latency | Hours to days per training run. | Tens of milliseconds per request. |
| Cadence | Retrain 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. |
| Validation | Offline 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):
| Step | Budget (p99) | How it stays within budget |
|---|---|---|
| Candidate generation (parallel sources) | ~30 ms | Precomputed timelines in Redis; ANN indexes in memory; sources run concurrently with per-source timeouts (a slow source is simply skipped). |
| Feature fetch | ~20 ms | Batched multi-get from online store; per-request caches; precomputed user features fetched once. |
| Lightweight ranking | ~10 ms | Tiny model, vectorised scoring of thousands of items. |
| Heavy ranking | ~50 ms | Batch all candidates into one model call; split across workers; GPUs/accelerators; quantised models. |
| Re-ranking & hydration | ~15 ms | Simple 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:
- Position bias: position 1 gets more clicks just for being first. Log position as a training feature (fixed at serving time) or use inverse-propensity weighting.
- Exploration: reserve a few slots for items the model is uncertain about (epsilon-greedy, Thompson sampling) so it keeps learning about new content.
- Selection bias: you only have labels for items you showed; a small randomised traffic slice yields unbiased data.
- Goodhart's law: optimising raw clicks rewards clickbait. Balance with surveys ("worth your time?"), negative signals (hide, report) and integrity demotions.
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
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:
- IVF (clustering): k-means splits vectors into, e.g., 65,536 cells; a query searches only the few dozen cells whose centroids are closest — ~0.1% of the data.
- Product quantisation (PQ): compresses each 512-byte vector to ~16–64 bytes of codebook IDs, so a billion vectors fit in tens of GB of RAM.
- HNSW: a multi-layer neighbour graph; a greedy walk from the top layer reaches the query's neighbourhood in ~log(N) hops. Excellent recall/latency, memory-hungry.
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:
| Case | Problem | Techniques |
|---|---|---|
| New user | No 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 creator | No 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:
- Diversity and exploration in re-ranking (topic caps, novelty bonuses).
- Recommendation eligibility: some content may stay on the platform but not be recommended to non-followers; Meta has publicly published recommendation guidelines of this kind.
- Integrity classifiers (spam, nudity, violence, misinformation) in the media pipeline and in ranking.
- User controls: "not interested", sensitive-content settings, a non-personalised/chronological view (the EU Digital Services Act requires a non-profiling option for very large platforms).
- Long-term metrics and surveys, not only short-term clicks.
13. How Instagram uses analytics & ML
- 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.
- 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.
- Metrics & experiments. Daily pipelines compute DAU/MAU, retention and creator metrics; an experimentation platform evaluates every launch.
- 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.
- 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.
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
- Analytics on the production database — scans that evict the hot cache and slow the app. Use CDC/events into a separate store.
- Grouping by processing time instead of event time; or no watermark strategy, so late events vanish or windows never close.
- Not de-duplicating at-least-once events → inflated counts.
- Training–serving skew and label leakage — use a feature store with point-in-time joins.
- Trusting offline metrics — always confirm with an online A/B test.
- Forgetting deletion — user data must also leave the lake and feature stores (see Security & Privacy).
15. Real-world technologies
| Technology | Category | When to use |
|---|---|---|
| Kafka + Schema Registry, Debezium | Events, CDC | Replayable event streams; DB changes out of OLTP stores. |
| S3 / HDFS + Parquet; Iceberg / Delta / Hudi | Lake storage, table formats | Cheap columnar storage; ACID, time travel and row-level deletes. |
| Snowflake, BigQuery, Redshift | Cloud warehouses | Managed SQL analytics. |
| Spark, Hive; Presto / Trino | Batch; interactive SQL | Large ETL and training data; ad-hoc queries over the lake. |
| Flink, Kafka Streams | Stream processing | Windows, fresh features, spam signals. |
| Druid, Pinot, ClickHouse | Real-time OLAP | Sub-second dashboards on fresh events. |
| Airflow; Feast / Tecton | Orchestration; feature store | Scheduling batch DAGs and backfills; consistent online/offline features. |
| PyTorch, XGBoost; FAISS, ScaNN, Milvus | Training; vector search | Deep 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.
- Events (client + service + CDC) are the raw material; log them with IDs, two timestamps and schemas, and expect duplicates and late arrivals.
- Keep OLTP (serve the app) and OLAP (analyse history) separate; analytics lives on columnar, partitioned storage in a lake/warehouse/lakehouse.
- Batch = exact and cheap but late; stream = fresh but must handle event time, windows and watermarks. Lambda runs both; Kappa runs one replayable stream.
- Measure with DAU/MAU, cohorts and guardrails; prove changes with properly powered A/B tests (n ∝ 1/δ²).
- Ranking is a funnel (retrieval → light → heavy → re-rank) under a strict latency budget, with a feature store preventing skew and leakage.
- Explore = embeddings + two-tower + ANN; design deliberately for cold start, feedback loops, filter bubbles and integrity.
Related lessons
- Message Queues, Pub/Sub & Kafka — the event bus that feeds every pipeline here
- News Feed: Fan-out & Ranking — where the ranking funnel is served
- Search & Inverted Indexes — retrieval and trending signals for search
- Databases: SQL vs NoSQL — the OLTP side of the OLTP/OLAP split
- Caching — Redis timelines and online feature serving
- Monitoring, Logging & Tracing — operational telemetry vs product analytics
- Security & Privacy — deletion and minimisation across analytical stores