sehaj/learn
System design/12 Realtime, feeds and location

12.03 · concept · 12 min

Feed generation: push, pull, hybrid

Building a home feed of posts from everyone a user follows: push each post into followers' feeds, pull at read time, or decide per author. Tested hybrid code, what to store in a feed, and ranking.

Snip Bio's app opens on a feed: the latest links posted by creators you follow, newest first. Most users follow 20 to 300 creators. Most creators have a few hundred followers; a handful have millions. People open the app far more often than creators post. The feed must load in under 200 ms.

Push: build feeds on write

When a creator posts, a worker appends the post's ID to the stored feed of every follower: a list per user, newest first, capped at a few hundred entries (older posts are rarely scrolled to, and can be fetched the slow way). Opening the app is then one cheap read of a precomputed list.

The cost is on write: one post from a creator with 2 million followers is 2 million feed writes, and it takes time for the last follower's feed to get it.

Pull: build feeds on read

Store each creator's posts once. When a user opens the app, fetch the recent posts of everyone they follow and merge them. Posting is one write. Reading is a fan-out to hundreds of creators and a merge, on every open, which is slow and expensive when reads vastly outnumber posts.

Hybrid: decide per author

Yahoo's "Feeding Frenzy" paper made this precise: the minimum total cost comes from deciding push or pull separately for each pair of producer and consumer, based on how often the producer posts compared with how often the consumer reads. In practice it usually reduces to a simple rule: push for ordinary creators, pull for creators with huge followings, and merge the two at read time.

ts
type Post = { id: number; author: string; at: number };

export class Feeds {
  private posts = new Map<string, Post[]>(); // author -> their posts, newest last
  private inbox = new Map<string, Post[]>(); // follower -> pushed posts, newest last

  constructor(
    private readonly followers: Map<string, string[]>, // author -> followers
    private readonly following: Map<string, string[]>, // user -> authors they follow
    readonly celebrityThreshold = 1000,
    readonly keep = 800,
  ) {}

  private isCelebrity(author: string) {
    return (this.followers.get(author)?.length ?? 0) >= this.celebrityThreshold;
  }

  publish(p: Post) {
    this.posts.set(p.author, [...(this.posts.get(p.author) ?? []), p]);
    if (this.isCelebrity(p.author)) return; // too many followers: they'll pull it at read time
    for (const f of this.followers.get(p.author) ?? []) {
      const box = this.inbox.get(f) ?? [];
      box.push(p);
      if (box.length > this.keep) box.shift(); // keep only the newest posts per feed
      this.inbox.set(f, box);
    }
  }

  read(user: string, limit = 20): Post[] {
    const pushed = this.inbox.get(user) ?? [];
    const pulled = (this.following.get(user) ?? [])
      .filter((a) => this.isCelebrity(a))
      .flatMap((a) => (this.posts.get(a) ?? []).slice(-limit));
    return [...pushed, ...pulled].sort((a, b) => b.at - a.at || b.id - a.id).slice(0, limit);
  }
}

Tested with 3,000 users, each following up to 20 accounts with a skewed popularity (so a few accounts had thousands of followers), and 5,000 posts: with a celebrity threshold of 500 followers, only 5 accounts were pulled, yet pushing fell from 426,487 feed writes to 187,176, and for 300 users checked against a brute-force feed, every one matched.

What a feed stores

  • IDs, not posts. A feed entry is a post ID and a timestamp (maybe the author). Post content is fetched in a batch from a cache when the feed is read, so edits and deletions show up immediately and feeds stay small.
  • Capped length. A few hundred entries per user. Scrolling past that falls back to the pull path.
  • Only for active users. Building feeds for people who haven't opened the app in a month is wasted work. Skip them on write and build their feed by pulling when they return.

Ranking

Newest first is the simplest order. Many feeds instead rank: a model scores the candidate posts from push and pull by predicted interest (relationship with the author, engagement so far, freshness), then sorts. The architecture stays the same: fan-out gathers candidates; ranking orders them at read time.

Seen in production

Yahoo! Research

2010

Feeding Frenzy

The paper defines push as writing events into materialized per-consumer feeds, and pull as fetching from per-producer event stores at query time. It notes that fan-out multiplies load (one celebrity's update then reached over 4.6 million followers) and that fan-outs and update rates are highly skewed. Its analysis shows the global cost is minimised by making a local decision for each producer-consumer pair based on the ratio of the producer's update rate to the consumer's query rate, and experiments on Yahoo's PNUTS showed this hybrid gave the lowest system load.

What we foundDecide push or pull for each producer-consumer pair from their relative event and query rates; the hybrid gives the lowest load.

Feeding Frenzy: Selectively Materializing Users' Event Feeds (Silberstein, Terrace, Cooper and Ramakrishnan, SIGMOD 2010)jeffterrace.com ↗
Predict first

Snip pushes every post to every follower. A creator with 3 million followers posts, and followers see it up to 10 minutes late, while the feed workers' queue backs up for everyone. What's the fix?

Commit to an answer before you read on.

Recap

Interview questions

SeniorfreqMust-knowDesign

Design the home timeline for a social network like Twitter.

Report a mistakeEdit this page