- Instagram's follow graph is a directed graph: users are nodes, "A follows B" is an edge A→B. There are hundreds of billions of edges, read far more often than written.
- Store each edge twice: in A's following list (sharded by A) and B's followers list (sharded by B), so both directions are single-shard reads. Keep the two in sync with an outbox/async repair (dual writes).
- Facebook's TAO (public paper) is the canonical design: objects + associations in sharded MySQL behind a massive, region-aware write-through cache, with typed inverse edges and cached counts.
- Core queries: following/followers lists (cursor-paginated), "does A follow B" (point lookup), mutuals (set intersection), counts (denormalised), and suggestions (friends-of-friends, computed offline).
- Hard parts: celebrities with hundreds of millions of followers (wide partitions, fan-out), count consistency, private accounts/requests, blocking, close friends — and feeding the fan-out workers.
1. The problem: why does "who follows whom" need its own service?
Almost every Instagram feature asks the graph a question:
- Feed: "who follows the author of this new post?" (to fan it out) and "whom do I follow?" (to pull celebrity posts).
- Profile: follower/following counts, "Followed by alice, bob and 12 others", the Follow/Following/Requested button.
- Privacy: "may this viewer see this private account's posts?", "has either blocked the other?".
- Stories: tray membership and close-friends audiences. Search and Explore: rank accounts you're connected to higher. DMs: requests vs inbox depending on whether you follow each other.
That makes graph reads one of the highest-QPS workloads in the system — every profile view, comment and feed refresh triggers several — while follows/unfollows are comparatively rare. And the data is skewed: a typical user follows a few hundred accounts, but the largest accounts have hundreds of millions of followers.
2. Modelling the graph
A graph is a set of nodes (vertices) connected by edges. Instagram's follow graph is directed (Alice following Bob doesn't mean Bob follows Alice — unlike Facebook friendship, which is undirected/symmetric) and edges carry a little data: creation time, state (active/pending), maybe a close-friends flag.
- Out-degree of a node = how many accounts it follows (Instagram caps this at 7,500 to curb spam). In-degree = follower count — unbounded, and extremely skewed (a power-law distribution).
- Adjacency list: the list of neighbours for each node — the natural storage form for sparse graphs (an adjacency matrix of 2B × 2B would be absurd).
2.1 Rough numbers (assumptions)
Accounts with edges ~1.5B
Average following ~200 → 300B directed edges
Stored in both directions 600B rows × ~25 B ≈ 15 TB (× 3 replicas ≈ 45 TB, plus indexes)
Follow + unfollow writes 500M DAU × 0.2/day ≈ 100M/day ≈ 1,200/s avg
Graph reads feed fan-out, profile views, permission checks, mutuals
→ easily several hundred thousand to millions of lookups/s
Read : write ≈ 1,000 : 1 (or more)
The data fits comfortably on a sharded cluster; the challenge is read throughput, latency and skew, which is why caching dominates graph design.
3. Storage options
3.1 One relational table with two indexes
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followee_id BIGINT NOT NULL,
state SMALLINT NOT NULL, -- 1 = active, 2 = pending request
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (follower_id, followee_id) -- "whom does A follow?", "does A follow B?"
);
CREATE INDEX follows_by_followee ON follows (followee_id, created_at DESC); -- "who follows B?"
On a single database this is perfect: one row per edge, both directions indexed, follow/unfollow is one transaction, counts are a query (or a trigger-maintained column). It's how most apps start; early Instagram scaling talks describe a follow graph that began as essentially this kind of simple (source_id, target_id, status) table in PostgreSQL.
It breaks when you shard: if the table is sharded by follower_id, "whom does A follow" hits one shard, but "who follows B" must ask every shard (scatter-gather), because B's followers are spread everywhere. One of the two directions always becomes a cross-shard query.
3.2 Two tables, each sharded by its own user (dual writes)
The standard fix is to store the edge twice, each copy sharded by the user it "belongs" to:
- Reads are always local: following(A) lives on A's shard; followers(B) on B's shard.
- Writes touch two shards with no shared transaction (a dual write). If the second write fails, the copies disagree. Strategies:
- Treat one side as the source of truth (usually
following, the side the user controls), and write it together with an outbox event in one local transaction; a consumer applies the inverse edge idempotently, retrying until done. - Or write both synchronously and run an anti-entropy/repair job that scans for edges missing their inverse — the approach TAO takes.
- Treat one side as the source of truth (usually
- Storage doubles — a cheap price for halving the fan-out of every read.
Wide-column stores like Cassandra fit this naturally: partition key = user_id, clustering key = (created_at DESC, other_id), giving newest-first paged lists per user.
3.3 Graph databases
A graph database (Neo4j, Amazon Neptune, JanusGraph, TigerGraph) stores nodes and relationships natively, using index-free adjacency (each node points directly at its edges), and offers query languages like Cypher or Gremlin for multi-hop traversals:
// Cypher: accounts followed by people I follow, that I don't follow yet, ranked by overlap
MATCH (me:User {id: 1001})-[:FOLLOWS]->(f)-[:FOLLOWS]->(s)
WHERE NOT (me)-[:FOLLOWS]->(s) AND s <> me
RETURN s.id, count(f) AS mutuals ORDER BY mutuals DESC LIMIT 20
Great for deep traversals, fraud rings and knowledge graphs. But Instagram's online queries are almost all one hop (lists, edge checks, counts), at enormous QPS; general-purpose graph databases are hard to shard across hundreds of machines (a traversal crosses partitions constantly), and the multi-hop analyses (suggestions) can run offline in batch systems instead. That's why the largest social networks built sharded key-value/relational stores with graph-shaped APIs rather than using a graph database online.
3.4 Facebook's TAO: objects and associations
TAO ("The Associations and Objects", USENIX ATC 2013) is Meta's publicly described graph store serving Facebook's social graph — billions of reads per second, overwhelmingly reads. Its model:
- Objects:
id → (otype, key-value data)— users, posts, comments, places. - Associations:
(id1, atype, id2) → (time, key-value data)— typed, directed edges such as FOLLOWS, LIKES, AUTHORED. An association list is all edges of one type from id1, ordered newest-first. - Inverse types: FOLLOWS (A→B) can be configured with an inverse FOLLOWED_BY (B→A); TAO writes both. The two writes aren't atomic; a background job repairs "hanging" one-sided edges.
- A small API:
assoc_add,assoc_delete,assoc_get(id1, atype, id2set)(edge check),assoc_count(id1, atype),assoc_range(id1, atype, pos, limit),assoc_time_range(id1, atype, high, low, limit).
- Storage: sharded MySQL; an object's ID encodes its shard, and an association is stored on the shard of
id1, so association lists are single-shard. - Caching: two cache layers. Follower tiers absorb reads; on a miss they ask the shard's leader cache, which reads the database. Writes go through the leader (write-through), which updates the DB and invalidates/refreshes followers. Counts (
assoc_count) are cached alongside lists. - Regions: one region holds each shard's primary DB; other regions hold replicas and forward writes to the primary region. Reads are eventually consistent across regions, with read-after-write for your own writes in practice.
- Why it works: the workload is >99% reads, associations are small, and most queries hit recent edges — so caching achieves very high hit rates.
3.5 Comparison
| Option | Strengths | Weaknesses | Use when |
|---|---|---|---|
| Single follows table + 2 indexes | Simple; atomic follow; easy counts | One direction is cross-shard once sharded | Up to the limits of one (replicated) DB |
| Following + followers tables, each sharded by own user | Both directions single-shard; scales horizontally | Dual writes need repair; 2× storage | Large scale, simple one-hop queries |
| Graph database | Expressive multi-hop traversals | Hard to shard; lower throughput for simple lookups at extreme QPS | Analytics, fraud, recommendation research, moderate scale |
| TAO-style association store | Graph API + massive cache + regions; typed edges for many features | Big engineering investment; eventual consistency | Planet-scale social products |
4. The queries the graph must answer
| Query | Where it's used | How (two-table / TAO model) | Cost |
|---|---|---|---|
| Following list of A | "Following" screen, feed pull for celebrities, tray | Range scan on A's shard: following WHERE user=A ORDER BY created_at DESC LIMIT 50 / assoc_range | 1 shard, cached |
| Followers list of B | "Followers" screen, fan-out workers | Range scan on B's shard with a cursor | 1 shard (bucketed for celebrities) |
| Does A follow B? | Follow button, private-account access, DM inbox vs requests, comment permissions | Point lookup following(A, B) / assoc_get(A, FOLLOWS, {B}) | O(1), very hot → cache incl. negative results |
| Mutuals | "Followed by alice, bob + 12 others" on B's profile | Intersect following(viewer) with followers(B) (below) | Bounded by viewer's following size |
| Counts | Profile header | Denormalised counters / assoc_count | O(1) |
| Suggestions | "Suggested for you", People You May Know | Offline friends-of-friends + ML, served from a precomputed store | Batch compute; O(1) serve |
4.1 Mutuals: "Followed by …"
When Alice views Bob's profile, Instagram shows which of the people Alice follows also follow Bob: following(Alice) ∩ followers(Bob). Bob may have 50M followers, Alice ~300 followings — so iterate the small set and probe the big one:
def followed_by(viewer, profile, limit=3):
mine = graph.following_ids(viewer) # ~300 IDs, cached list
# one batched probe on profile's shard: which of these follow `profile`?
hits = graph.followers_contains(profile, candidates=mine) # SELECT ... WHERE user=profile AND src IN (...)
ranked = rank_by_closeness(viewer, hits) # show closest friends first
return ranked[:limit], len(hits) # "Followed by a, b, c + N others"
Cost: one multi-get of ~300 keys on a single shard instead of reading 50M rows. "Mutual follow" (A follows B and B follows A) is just two point lookups.
4.2 Suggestions: friends-of-friends and People You May Know
The classic signal is triadic closure: if many people you follow follow X, you probably know or would like X. Computing it online is too expensive — 300 followings × 300 followings each = 90,000 candidate edges per request — so it's done offline (Spark/batch over a graph snapshot in the data warehouse, see Analytics & ML) and served from a precomputed store:
- Candidate generation: friends-of-friends (2-hop), uploaded contacts (with consent), accounts that follow you but you don't follow back, co-follow similarity ("people who follow X also follow Y"), location/school signals, embeddings + approximate nearest neighbours.
- Scoring: number of mutual connections, weighted so that mutual friends with few connections count more — e.g. Adamic–Adar
score(u, x) = Σz ∈ mutuals 1 / log(degree(z))— plus an ML model predicting follow-back and long-term engagement. - Filtering: remove already-followed, blocked, previously dismissed, private-policy violations, spam accounts.
- Serving: store the top ~100 per user in a key-value store; refresh daily; add real-time suggestions after a follow ("similar accounts to the one you just followed") from a co-follow index.
5. Celebrity accounts
The largest Instagram accounts have over 600 million followers. Everything that's trivial for a normal user becomes an engineering problem:
- Wide partitions: a followers partition keyed only by user_id would hold 600M rows × ~25 B ≈ 15 GB on one replica set — far beyond what Cassandra-style stores handle well (partitions are best kept well under ~100 MB). Fix: bucket the key, e.g.
(user_id, bucket)where bucket is a time range of follow date orhash(follower_id) % N, with N chosen per account size. - Pagination only: nobody (human or service) may load the whole list. The UI pages 50 at a time with an opaque cursor
(bucket, created_at, follower_id), and offers search within followers instead of endless scrolling. - Fan-out: pushing a celebrity's post into 600M timelines would take 600M writes — the news feed switches to pull for such authors (section 8).
- Hot counters: a celebrity gains thousands of followers per minute; their follower count is a hot counter (section 6).
- Hot edge checks: "does viewer follow Celeb?" is asked by millions of profile views — but it's keyed by (viewer, Celeb) on the viewer's following list, so the load is naturally spread. Good key design matters.
6. Follower count consistency
Counting followers(B) on each profile view would scan millions of rows, so counts are denormalised: follower_count and following_count stored per user and updated on follow/unfollow. Problems and fixes:
- Hot counter: a celebrity gaining 5,000 followers/minute serialises on one row — use the same tricks as like counters: stream aggregation of FollowCreated/FollowRemoved deltas, sharded counters, Redis INCR + flush.
- Drift: retries, partial dual writes and bugs make counters disagree with the edge lists. Derive deltas only from actual state transitions, and run periodic reconciliation (recount edges for accounts whose counters look suspicious, or sample-check).
- Eventual consistency: the public count may lag by seconds; large counts are shown rounded ("612M"). The user's own following count should reflect their action immediately (read-your-writes via optimistic UI).
- Spam cleanup: purging fake accounts removes their edges in bulk, so counts drop in batches — another reason to aggregate deltas rather than lock rows.
7. Private accounts, follow requests, blocking and close friends
7.1 Follow requests
For a private account, following is a two-step handshake. Model the request as its own edge type with a small state machine:
STATE EVENT NEXT STATE EDGES WRITTEN ────────── ──────────────────────────────────── ────────── ───────────────────────────────────── NONE follow (public account) FOLLOWING + FOLLOWS(A→B), FOLLOWED_BY(B←A) NONE follow (private account) REQUESTED + FOLLOW_REQUEST(A→B), INCOMING_REQ(B←A) REQUESTED owner accepts FOLLOWING − request edges, + follow edges REQUESTED owner declines / requester cancels NONE − request edges FOLLOWING unfollow / owner removes follower NONE − follow edges any block (either direction) NONE − all follow/request edges, + BLOCKS
- Accepting deletes the request edges and adds the follow edges (plus counts and a
FollowCreatedevent) — a small saga across shards, made idempotent so a double-tap on "Confirm" is harmless. - Switching private → public auto-approves pending requests (a background job, since there may be thousands).
- Access checks: a private account's posts, stories and follower lists are visible only if
does_follow(viewer, owner)is true and active (not pending).
7.2 Blocking
A block (A blocks B) is a strong, symmetric-in-effect relationship stored as its own edge type (BLOCKS with inverse BLOCKED_BY):
- On block: remove follow edges in both directions, remove pending requests, adjust counts, and emit events so feeds/trays drop each other's content.
- On every read path (feed, profile, search, comments, likes lists, stories, DMs) check
blocked(A, B) OR blocked(B, A). These checks are extremely frequent, so block lists are small, cached per user, and loaded once per request. - Blocking must win races: if B's post was already fanned into A's timeline, the read-time filter hides it.
7.3 Close friends, mute and restrict
| Relationship | Visible to the other person? | Effect |
|---|---|---|
| Close friends (owner → friend) | No (owner-private list; the friend just sees a green ring) | Audience for close-friends stories |
| Mute posts/stories | No | Excluded from the muter's feed/tray; still following |
| Restrict | No | Their comments visible only to themselves unless approved; DMs go to requests |
| Block | Effectively yes (content disappears) | Severs follows both ways; hides everything |
All are just more typed edges in the same store — which is exactly why a TAO-style "associations with types" model is so convenient: one system, one API, many relationship kinds.
8. How fan-out workers read the graph
When a PostCreated event arrives, the feed fan-out workers must insert the post ID into every follower's timeline. The graph is their main input:
- Check size: if B's follower count exceeds a threshold (say 1M), skip push entirely — B is a "celebrity" and followers will pull B's recent posts at read time (the feed service calls following(viewer) to find which celebrities to merge).
- Page through followers: read followers(B) in pages of ~1,000 via cursor; each page becomes an independent job on a queue, so a 500k-follower fan-out is parallelised across many workers and resumable if one crashes.
- Filter: skip inactive followers (no login in 30 days — they'll get a rebuilt feed on return), muted relationships, blocks, audience restrictions.
- Batch writes: group follower IDs by timeline cache shard and pipeline
ZADDs; trim timelines to the latest N entries. ZADD is idempotent, so retries are safe.
Rough load: 100M posts/day × ~200 followers ≈ 20B timeline inserts/day ≈ 230k/s — which is why follower reads for fan-out are served from cache or bulk-scanned efficiently, and why the celebrity cut-off matters.
9. Follow / unfollow event flow
App (Alice) Graph service Alice's shard Kafka Consumers | POST /follow Bob | | | | |------------------->| check: blocked? private? | | | | | rate limit (anti-spam) | | | | |-- tx: following(alice,bob) | | | | | + outbox FollowCreated -->| | | |<-- 200 following --| |-- relay ------->| | | (optimistic UI) | | |-- inverse writer: followers(bob, alice) | | | |-- counters: alice.following+1, bob.followers+1 | | | |-- notifications: "alice started following you" | | | |-- feed backfill: bob's recent posts → alice's timeline | | | |-- PYMK / search affinity / analytics
- Feed backfill: after following, the new followee's recent posts should appear in your feed. A consumer inserts B's last few posts into A's timeline (or the feed simply merges them at read time).
- Unfollow mirrors it: delete the edge (+ outbox
FollowRemoved); consumers remove the inverse edge, decrement counts, and remove B's posts from A's timeline — or, more cheaply, the feed filters them out at read time against the current following list. - Idempotency: following someone you already follow creates no new edge and emits no event; consumers dedupe on event ID.
- Abuse: follow/unfollow churn is a classic spam tactic, so the API enforces per-account rate limits and follow caps (see Rate limiting).
10. Sharding and caching the graph
10.1 Sharding
- Shard key = the owning user's ID for each adjacency list: following(A) on shard(A), followers(B) on shard(B). Instagram's 64-bit IDs embed a logical shard number (ID generation), and logical shards map onto physical databases, so data can be rebalanced by moving logical shards.
- Co-location: keeping all of one user's edge types (follows, blocks, close friends, requests) on that user's shard means a request about one user's relationships touches one shard.
- Celebrity buckets: very large follower lists are split into sub-partitions (section 5).
- Consistent hashing or a directory of logical → physical shards makes adding capacity cheap (Consistent hashing).
10.2 Caching
| What | Shape | Notes |
|---|---|---|
| Following ID list | following:{A} → ~200 IDs (≈ 1.6 KB) | Small and read constantly (feed pull, mutuals); cache whole list |
| Edge existence | edge:{A}:{B} → yes/no | Negative caching matters: most "does A follow B" answers are "no" |
| Counts | counts:{B} → followers, following | Updated by the aggregator; no per-follow invalidation for hot accounts |
| Followers list pages | First page(s) only | Never cache 600M-entry lists whole |
| Block list | blocks:{A} → small set | Loaded once per request for read-time filtering |
Writes use write-through or invalidate-on-write to the owner's cache entries (TAO's leader cache serialises writes per shard to keep caches coherent). Across regions, caches are eventually consistent; a user's own follow is reflected immediately on their device via optimistic UI and, server-side, by routing their reads to fresh data for a short window after a write. See Caching and Replication.
11. How Instagram uses the social graph
FollowCreated/FollowRemoved events to Kafka. Its biggest client is the Feed Fan-out Workers, which read follower lists to push post IDs into Redis timelines (with pull for celebrities); the Feed, Stories, Search and DM services call it for following lists, edge checks, blocks and close-friends audiences; the Notification Service sends "X started following you"; and Analytics & ML uses graph snapshots for suggestions and ranking. Instagram's early scaling talks describe a follow graph stored in a simple relational table on sharded PostgreSQL; at Meta, the publicly documented reference design for social-graph data is TAO — objects and typed associations in sharded MySQL behind a huge, region-aware cache. Whatever the exact store behind a given feature today, the diagram's "Social Graph store" is best understood as a TAO-like system: adjacency lists keyed by user, stored in both directions, served almost entirely from cache.- Scale (assumed): hundreds of billions of edges; graph reads in the millions per second across all features; follows/unfollows in the low thousands per second.
- Limits: Instagram caps how many accounts one user can follow (7,500), bounding out-degree for fan-in reads and mutuals — while in-degree (followers) is unbounded and handled with bucketing and pull-based fan-out.
- Privacy: private accounts, requests, blocks, restrict, mute and close friends are all typed edges checked on every read path.
12. Trade-offs & common pitfalls
✅ Two-direction, user-sharded adjacency lists give you
- Single-shard reads for both "following" and "followers"
- Horizontal scaling to hundreds of billions of edges
- Simple, cacheable one-hop queries at huge QPS
- A uniform model for many relationship types (follow, block, close friends)
⚠️ At the cost of
- Dual writes that can diverge and need repair
- 2× storage
- Eventually consistent counts and cross-region views
- Multi-hop queries (suggestions) pushed to offline pipelines
- Special handling for celebrity-sized lists
Common pitfalls
- Sharding one follows table by follower only → every "who follows B" query is a scatter-gather across all shards.
COUNT(*)for follower counts on profile views.- Unbucketed celebrity partitions → 15 GB partitions, slow compactions, timeouts.
- Loading entire follower lists into memory in fan-out or "export" features.
- Checking blocks/privacy only at write time → content leaks after relationships change.
- No negative caching for edge checks → most lookups ("no, A doesn't follow B") hit the database.
- Computing friends-of-friends online per request.
13. Real-world technologies
| Technology | When to use it |
|---|---|
| Sharded PostgreSQL / MySQL | Follows tables with composite keys; strong per-shard consistency; the base under TAO |
| Cassandra / ScyllaDB | Wide-row adjacency lists partitioned by user, newest-first clustering |
| Facebook TAO (paper) | Reference design: objects + associations + write-through, region-aware cache |
| Twitter FlockDB (historical, open-sourced) | An earlier example of a sharded adjacency-list store for follow graphs |
| LinkedIn's graph services (publicly described, e.g. LIquid) | In-memory graph serving for connections and degree-of-separation queries |
| Neo4j, Amazon Neptune, JanusGraph, TigerGraph | Multi-hop traversals, fraud/abuse rings, moderate-scale graph apps |
| Redis / Memcached | Caching following lists, edge checks (incl. negative), counts, block lists |
| Spark GraphX / GraphFrames, Apache Giraph | Offline graph analytics: friends-of-friends, PageRank-style scores, PYMK candidates |
14. Interview corner
How would you store the follow graph for 2 billion users?
As adjacency lists stored in both directions: following(user_id, created_at, target_id) sharded by user_id, and followers(user_id, created_at, source_id) sharded by user_id. Each direction is a single-shard, newest-first range scan; "does A follow B" is a point lookup in following(A). Writes are dual: write following(A,B) plus an outbox event in one local transaction, and an idempotent consumer writes followers(B,A), counts and notifications; a repair job fixes any one-sided edges. Put a large cache in front (following lists, edge checks with negative caching, counts) — essentially the TAO design.
Why not just use a graph database like Neo4j?
Online queries are almost all one hop (lists, edge checks, counts) at millions of QPS with strict latency; that's a sharded key-value/relational access pattern, best served by partitioned adjacency lists plus caching. General graph databases shine at deep traversals but are hard to partition at hundreds of billions of edges, since traversals constantly cross partitions. Multi-hop work like friends-of-friends runs offline in Spark over graph snapshots instead. A graph DB is still a fine choice for analytics or abuse investigations.
An account has 600 million followers. What changes?
Storage: bucket the followers partition (time or hash buckets) to avoid a 15 GB partition. Reads: cursor pagination only, no full loads; search within followers in the UI. Feed: skip push fan-out; followers pull this account's recent posts at read time. Counters: stream-aggregated or sharded, shown rounded. Notifications: aggregate instead of one per follow. Caches: never cache the full list; cache counts and first pages. Edge checks remain cheap because they're keyed on the viewer's following list.
How do you compute "Followed by alice, bob and 12 others" on a profile?
It's following(viewer) ∩ followers(profile). Take the viewer's cached following list (bounded, ≤7,500, typically hundreds), and do one batched membership probe against followers(profile) on the profile's shard — a multi-get of a few hundred keys rather than scanning millions of followers. Rank the hits by closeness to the viewer and show the top few plus a count. Cache the result briefly per (viewer, profile).
How do you keep the following and followers copies consistent?
Choose a source of truth (following, which the actor controls). Write it and an outbox event atomically on the actor's shard; a consumer applies the inverse edge idempotently (upsert/delete) with retries, so they converge. Events are keyed by the edge pair so follow/unfollow for the same pair stay ordered. A periodic anti-entropy job scans for edges lacking their inverse (TAO calls these hanging associations) and repairs them. Reads that require authority (e.g. private-account access) consult the source-of-truth side.
How would you build "People You May Know"?
Offline candidate generation over a graph snapshot: friends-of-friends (2-hop), contacts (with consent), follow-backs, co-follow similarity and embedding neighbours. Score with features like mutual-count and Adamic–Adar, plus an ML model predicting follow and engagement; filter out existing follows, blocks, dismissed and spammy accounts. Store the top ~100 per user in a key-value store refreshed daily, and add real-time "similar accounts" suggestions after each follow from a co-follow index.
Walk through what happens when Alice blocks Bob.
The Graph service writes BLOCKS(Alice→Bob) and its inverse, deletes follow edges in both directions and any pending requests (idempotently, as a small saga), and emits events: counters decrement, timelines/trays drop each other's content, notifications and DMs are suppressed, search hides profiles. Because fan-out may already have placed content, every read path also filters with cached block lists (checked in both directions), so the block takes effect immediately regardless of async cleanup.
15. Check your understanding
If the follows table is sharded by follower_id across 256 shards, how many shards does "list B's followers" touch? How does the two-table design change that?
Up to all 256 (scatter-gather), because B's followers are spread by their own IDs. With a followers table sharded by followee (B), it's 1 shard.
1.5B accounts follow 200 accounts each on average. How many rows exist in the two-table design, and roughly how much raw storage at 25 bytes per row?
300B edges × 2 directions = 600B rows; × 25 B ≈ 15 TB raw (≈ 45 TB with 3× replication, before indexes) — modest for a sharded cluster, so the real challenge is read throughput.
Why is negative caching important for "does A follow B?"
Most such checks (profile views of strangers, comment permission checks, DM routing) return "no". Without caching negatives, every "no" misses the cache and hits the database, so the cache barely helps. Caching "no" (and invalidating on follow) keeps those lookups in memory.
A celebrity's follower count is shown as "612M" while the true number changes by thousands per minute. Is that a bug?
No. The count is a denormalised, eventually consistent value (aggregated from follow/unfollow deltas and reconciled periodically) and is displayed rounded. Exactness at that scale is neither perceivable nor worth the write contention.
Scenario: Bob unfollows Carol, but Carol's post was already fanned out into Bob's timeline. What should Bob see, and how?
Bob shouldn't see new content from Carol. The FollowRemoved consumer removes Carol's entries from Bob's timeline asynchronously, and, to be correct immediately, the feed read path filters timeline entries against Bob's current following list (and blocks) before rendering.
Compute the Adamic–Adar contribution of two mutual connections with 50 and 5,000 followers respectively (use natural log).
1/ln(50) ≈ 1/3.91 ≈ 0.256; 1/ln(5000) ≈ 1/8.52 ≈ 0.117. Total ≈ 0.373. A mutual connection with few connections is stronger evidence that you know the candidate than a mutual "hub" account that follows everyone.
Why does Instagram's cap of 7,500 followings help the system?
It bounds out-degree, so following lists stay small enough to cache whole, mutuals computations (iterate following(viewer)) stay cheap, feed pull-merges are bounded, and spam bots can't follow millions of accounts. Followers (in-degree) remain unbounded, which is where bucketing and pull fan-out come in.
- The follow graph is a huge, skewed, read-dominated directed graph; almost all online queries are one hop.
- Store edges in both directions, each sharded by its owning user, and keep them in sync via outbox-driven dual writes plus repair — or use a TAO-style association store.
- Answer edge checks with point lookups and negative caching; compute mutuals by probing the big set with the small one; generate suggestions offline.
- Handle celebrities with bucketed partitions, cursor pagination, pull-based fan-out and aggregated counters.
- Private accounts, requests, blocks, mute, restrict and close friends are typed edges, enforced on every read path.
- Follow/unfollow events drive the inverse edge, counts, notifications, feed backfill and recommendations.
Related lessons
- News Feed — the main consumer of follower lists; push vs pull for celebrities
- Sharding — shard keys, wide partitions and scatter-gather
- Caching — write-through caches, negative caching and hot keys
- Databases — relational vs wide-column vs graph databases
- Likes & Counters — the same counter techniques for follower counts
- Stories — close-friends audiences and tray fan-out
- Microservices — the outbox pattern behind dual writes
- Analytics & ML — offline graph processing for suggestions
- Security & Privacy — blocking, private accounts and abuse prevention