Users can follow/unfollow others, view a paginated stream of posts from people they follow, with the most recent posts displayed first.

Newsfeed

Written by
Staff Eng at Meta
Published on
April 12, 2025

Functional Requirements

  • Users should be able to view a stream of posts from people they follow (with the most recently created posts at the top).
  • (support pagination) Navigate through feed content using pagination, viewing 20 posts per page, accessing older content through infinite scroll or pagination controls

Non-Functional Requirements

  • The system should be fast in serving view_feed requests. (p90 < 500 ms)
  • The system should be highly available in serving requests (99.99 uptime)
  • The system should be scalable to support X users/posts (TBD)
  • The system can adopt eventual consistency when generating newsfeed. (New posts should be generated into feed in less than 1 minute)
  • The system should be fault tolerant in handling component/infrastructure failures

Below the line

  • Feed personalization/recommendation through user behavior analysis

API

request a feed

GET /api/v1/feed Parameters: - cursor: string (optional, for pagination) - pageSize: int (default: 20, max: 50) Headers: - Authorization: Bearer {jwt_token} Response: 200 OK: { "posts": [Post], "nextCursor": string, "hasMore": boolean }

High-level Design

Generate Newsfeed

To build a scalable newsfeed system, we'll explore three approaches, evolving from a simple solution to an optimized solution that satisfies all of non-functional requirements.

Solution 1: Simple Pull Model (Fanout-on-read)

When a user requests their feed, the Newsfeed Service performs three operations:

  1. Retrieves the list of users they follow through the Follower Service
  2. Fetches recent posts from each followed user through the Post Service
  3. Merges and sorts these posts by creation time before returning them

To optimize this simple approach, we implement several improvements:

  • Database Index.
    • For the post table, we can use userId as the partition key and createdAt as the sort key. So all posts created by the user_id are stored in the same partition, enabling efficient retrieval of a user's recent posts. Additionally, we can create local secondary index on engagement metrics (such as #views, #likes) supporting alternative sorting options (sort by view_count, likes).
    • For follower table, we have two access patterns:
      • Query 1: Find all users that X follows; —> Who does X follow?
        SELECT toUser FROM Follower WHERE fromUser = X
      • Query 2: Find all followers of X; —> Who follows X?
        SELECT fromUser FROM Follower WHERE toUser = X
      • We can use indexes to optimize both access patterns (queries). For follower table, we can use fromUser as partition key and toUser as sort key. So 1st query is fast —> find all users that X follows. And all toUser entries for a specific user are stored together.
      • Then we can add global secondary index (GSI) where partition key is toUser and sort key is fromUser. This GSI allows efficient querying to find all followers of a specific user (toUser).
  • Cache Layer. We cache post data and follower data.
    • Follower cache stores user follow relationship, reducing database load for social graph queries (i.e: find all users that a given user follows or find all follows for a given user);
    • Post cache maintains recently accessed posts, improving read performance for popular content and reducing DB loads on reads.

And here is the workflow diagram for solution 1.

To further reduce latency and system load on generating feeds, we can use a technique — pre-compute or create feeds offline adaptively.

Using pre-compute to create feeds offline adaptively
  • Keyword-1: Precompute. We can employ multiple newsfeed generation workers. With a job scheduler, we can set newsfeed generation workers to run at a fixed frequency to fetch all user data and relevant post data offline (before user opens the app or requests for new feeds).
  • Keyword-2: Adaptively. Users have different or inconsistent behaviors. Some users login and request newsfeed a couple times in a day. There could also be users who login to the app only once (or only every few days). So to best use of compute resources and have a right balance between latency and compute costs, we can adaptively pre-compute feeds. For example:
    • For users who haven't logged in for more than 30 days, we will stop generating feeds for those "cold users". Or their newsfeed will be generated next time when they login.
    • For users who logins daily or request feeds a couple times in a day, we can set newsfeed workers to run a higher frequency to ensure optimal user experience (low wait time on getting a new feed).

With all improvements (database index + cache + pre-compute), this option works but does not scale (at Facebook or Twitter level). Why?

  1. Facebook has 1B DAU. Assume each user on average opens app (or requests newsfeed) 1 ~ 5 times a day. QPS (on newsfeed request) is 10K ~ 50 K per second. Our pre-configured read capacity on dynamoDB can not catch up on this read workload (resulting in read throttling exceptions). Even we could further increase read capacity on dynamoDB, this creates a significant read workload on DB.
  2. New posts are required to be generated in newsfeed within 1 minute. To stick with 1 minutes requirement and ensures cache is also populated with latest (last 1-minute) data, there will be lots of database polling even when there could be no change on post (or no new posts).

To address those challenges, we introduced solution 2 — Fanout on write (”the push model”).

Solution 2: Fanout on write (Push Model) + Write feed in DB (cache)

Different from fanout-on-read (where the feed generation request fans out to fetch posts from all users the given user follows), fanout-on-write pushes the new post content to all followers. Following diagram explains the workflow for fanout-on-write.

  • When [Post Service] processes a new post request, it writes new post to DB and also publishes a new post creation message to a message queue (AWS Kinesis Stream || Kafka).
  • Fan-out process:
    • [Fanout worker] consumes the message from the queue and retrieves the list of followers or the post’s author from follower cache.
    • [Fanout worker] directly writes to feed cache for each follower (for fast retrieval).
  • When a user opens the app or requests a new feed, the [Feed Service] retrieves the pre-computed feed from feed cache and returns the feed to user. Pagination is handled through cursor-based navigation of the cached feed.

Benefits and Drawbacks (of solution 2)

The benefit of this fanout-on-write process is that the users don’t have to go through their friend’s lists to acquire newsfeeds. In this approach, the number of read operations is significantly reduced. However, this approach introduces new challenges, particularly the “celebrity problem” where users with millions of followers create massive writes.

"Celebrity Problem"

A celebrity could have millions of followers. With this fanout-on-write approach, a new post made from a celebrity, will need to be written to million of followers' feed. One option is to batch write requests (group 100 ~ 200 writes requests as one batch). With strong hardware (memory), Redis can handle up to 50 K writes per second (and handle even more by use multiple redis cluster + sharding).

[Preferred] Solution 3: Hybrid Approach

Our hybrid approach combines the advantages of both pull and push models to create a scalable and efficient newsfeed system. The key insight is that different types of users require different feed generation strategies. We can classify users into three categories based on their characteristics and applies distinct newsfeed generation strategies for each:

  1. Regular users (with less than 5 K follower) — fanout-on-write
  2. For regular users with moderate follower counts (less than 5K followers), we employ the fanout-on-write approach. When these users create posts, the system immediately distributes them to their followers' feeds, ensuring quick access and optimal read performance for the majority of our user base.
  3. Celebrity accounts (with 5K or more followers) — fanout-on-read
  4. For celebrity accounts that have massive follower bases (5K or more followers), we switch to a fanout-on-read strategy. This prevents the system from being overwhelmed by the write amplification that would occur if we tried to update millions of follower feeds simultaneously. When users request their feeds, the system dynamically merges celebrity posts with their pre-computed feed content, striking a balance between performance and system resources.
  5. Cold users” (who haven’t logged in for more than 30 days) — “generate next time when they login”
  6. The system also optimizes for cold users – those who haven't accessed the platform for more than 30 days. Instead of continuously updating feeds for inactive users, we generate their feeds on-demand when they return to the platform. This approach significantly reduces unnecessary computation and storage costs while maintaining a good user experience for returning users.

In summary, to support this hybrid model, we implement a dynamic feed generation service that works as follows: When a user requests their feed, the service first retrieves their pre-computed feed containing posts from regular users they follow. It then augments this feed by fetching and merging recent posts from any celebrity accounts they follow. The final feed is sorted by timestamp before being returned to the user.

And here is our final design with everything discussed so far!

Deep Dive

The system should be fast in serving view_feed requests (P90 < 500 ms)

We achieved a fast feed retrieval performance through a combination of (1) pre-computation, (2) caching and (3) content delivery optimization with CDN.

  • Feed pre-computation combined with an effective caching system. For most users, their feeds are pre-computed and stored in the feed cache with a TTL of one minute, aligning with our consistency requirements. When a user requests their feed, the Feed Service first checks this cache, providing sub-millisecond response times for cache hits. For celebrity content that isn't pre-computed, we implement a separate caching strategy. Celebrity posts are stored in a dedicated cache with longer TTL values, as these posts typically have higher view counts and longer relevance periods. When generating feeds that include celebrity content, the system performs parallel retrievals, merging pre-computed feeds with celebrity content while maintaining our latency targets.
  • CDN Integration. We can leverage CDNs to further optimize content delivery across different geographical regions. The system stores frequently accessed content, particularly media attachments and celebrity posts, in CDN edge locations. When a user requests their feed, the CDN serves static content from the nearest edge location, significantly reducing latency for users across different regions. This is particularly effective for celebrity content that gets accessed frequently across various geographical locations.

The system should be highly available in serving requests (99.99 uptime)

  • Service-Level
    • For each micro-service (post service, feed service etc), we can deploy multiple service instances and setup monitoring with automatic failover capabilities.
    • In addition, we can employ load balancer to distribute traffics across service instances, automatically routing requests away from unhealthy instances.
  • Database & Cache Layer
    • For database, we propose to use AWS DynamoDB, which is fully managed database service where data is replicated asynchronously across multiple AZs (availability zones). So that data is always accessible through other AZs when one AZ fails.
    • For Caching Layer (Redis), we can enable master-slave replication where master node handles write operations while multiple slave nodes maintain synchronized copies of the data. For fault tolerance, we implement automatic failover mechanisms. Each Redis cluster runs a sentinel process that continuously monitors the health of master and slave nodes. If a master node fails, the sentinel orchestrates an automatic failover process: it promotes one of the healthy slave nodes to become the new master, updates the cluster configuration, and ensures all other nodes recognize the new master. This process typically completes within seconds, minimizing disruption to the service.

The system should be scalable to support X users/posts.

Assume we have 1 B DAU and each user requests feeds 5 times a day, then our QPS is 10^9 / 10^5 (seconds in a day) * 5 = 50 K. To support approximately 50 K QPS, we can implement scalability at every layer:

  • Service-level scaling. Both post service and feed service scales horizontally. We can dynamically provision and add more service instances based on existing resource utilization, processing latency, incoming traffics.
  • Data Storage Scaling. See our previous design on how to scale dynamoDB read/write capacities.
  • Caching Layer (Redis) Scaling. The primary scaling mechanism involves adding more nodes to the cluster, with a clear separation of write and read responsibilities. Master nodes handle all write operations, ensuring data consistency, while multiple slave nodes serve read requests. This architecture allows us to scale read capacity by adding more slave nodes as read traffic increases.When write throughput becomes a bottleneck, we implement horizontal sharding across multiple master nodes. Each shard handles a subset of users based on a consistent hashing strategy, allowing us to distribute write load evenly. As the system grows, we can add new shards to accommodate increased write volume while maintaining performance.
(Long-Version) Scale up Redis cluster to serve 1B DAU
Estimation:
  • Feed cache only stores a list of post ids (reference) and few other necessary metadata. We can estimate that to be 100 bytes. Each feed contains 50 ~ 100 posts. So the total feed cache per user would be 5 ~ 10 KB.
  • Post cache stores actual content and metadata (using 1 KB for estimation). 50 ~ 100 posts * 1 KB per post, would take 50 ~ 100 KB per user.
  • Sum them up. each user, takes 55 ~ 110 KB. (taking 80 KB for estimation)
  • A typical redis cluster node: 128 GB RAM. We use 110 GB for data storage (after system overheads + extra head room for operations). Then, max users can be handled by one Redis node would be: 110 GB / 80 KB = 1.4 M.
  • Assume 20% of 1B users are active users, so #nodes_needed = 200 M users / 1.4 ~= 143 master nodes. With 2 replicas, 143 * 3 ~= 430 total nodes.
Coach + Mock
Practice with a Senior+ engineer who just get an offer from your dream (FANNG) companies.
Schedule Now
Content: