The most surprising thing about designing a Twitter feed is that the "firehose" of tweets doesn’t actually get pushed to every follower; instead, it’s pulled on demand, but with a heavy dose of pre-computation.
Let’s see this in action with a simplified model. Imagine a user, Alice, tweets.
# Alice tweets
tweet_id = post_tweet("Alice", "Having coffee!")
# This tweet needs to appear on her followers' feeds.
# Let's say Bob and Carol follow Alice.
# We need to update Bob's and Carol's timelines.
update_timeline("Bob", tweet_id)
update_timeline("Carol", tweet_id)
# When Bob wants to see his feed:
bob_feed = get_timeline("Bob", count=20)
This update_timeline is where the magic and the complexity lie. It’s not a simple database insert for each follower. If Alice has millions of followers, doing millions of writes for a single tweet is prohibitively expensive. This is the core of the "fanout" problem.
The solution is a hybrid approach. For most users, when they tweet, that tweet is inserted into a data structure representing their own timeline. Then, when a user requests their feed, we fetch tweets from the timelines of the people they follow and merge them. This is called the "pull" model.
However, for highly influential users (celebrities, popular accounts), their tweets are proactively pushed to the timelines of their followers. This is the "push" model, also known as "fanout on write." This is because if a celebrity tweets, their tweet must appear on millions of follower timelines immediately, and the cost of fetching and merging on demand would be too high and too slow.
Here’s how the system is broken down internally:
- Home Timeline Service: This is the core service that generates a user’s "home timeline" – the stream of tweets they see. It’s responsible for fetching tweets from the users someone follows.
- Fanout Service: This service handles the distribution of tweets. It determines whether to fanout on write (push) or fanout on read (pull).
- Tweet Storage: Where individual tweets are stored. This is typically a distributed database.
- Timeline Storage: Where the pre-computed timelines (for the push model) or the list of followed users are stored. This is often a high-throughput key-value store like Redis.
- Caching Layer: Crucial for performance. User timelines, popular tweets, and even user profiles are heavily cached to reduce latency. Memcached or Redis are common choices.
Let’s consider the levers you control:
- Fanout Strategy: You decide for each user whether they use the push or pull model. This is usually based on follower count. For instance, users with > 10,000 followers might use push, while others use pull.
- Timeline Size: How many tweets are initially fetched and merged for a user’s home timeline.
- Cache Invalidation Strategy: When a user retweets or deletes a tweet, how quickly is that reflected in caches?
- Read vs. Write Optimization: The system is heavily optimized for reads (users viewing their feeds) because that’s the dominant traffic pattern.
To optimize the home timeline generation, we use a technique where we don’t just fetch the latest tweets from everyone a user follows. Instead, we might fetch tweets from the last 100 users followed, then merge and sort them. A more advanced approach involves fetching a fixed number of tweets (e.g., 800) from the users someone follows, then merging and de-duplicating them to produce the latest 500 for the user’s feed.
A key optimization for the "pull" model involves fetching not just the tweet IDs, but the actual tweet objects. To avoid fetching millions of tweets and then filtering, the system often fetches tweet IDs from the timelines of followed users, then fetches the actual tweet objects in batches for those IDs that are relevant. This reduces the number of individual object fetches.
The critical insight for scaling is that the "fanout" isn’t a single event; it’s a continuous process. When a user follows someone new, their timeline service needs to be updated to start pulling tweets from that new follow. This is often handled by a background process that backfills the new follower’s timeline with recent tweets from the followed user.
The next challenge you’ll face is handling retweets and likes efficiently, as these also need to be reflected in user timelines and can significantly increase the fanout load.