← System design

Social & Feed

Design Reddit

Reddit is thousands of separate communities sharing one engine. The engine has to rank posts by a mix of votes and age without re-sorting anything on page load, serve comment trees that can run to tens of thousands of nodes, and count a sudden rush of votes on one post without turning it into a locked row.
Core: precomputed hot lists plus batched vote deltas36,000 page loads a second at peakRead-dominatedBursty votes on a few posts

01

Scope of the platform

02

Scope of the platform

Functional
  • Users create communities, join them, and submit text, link, image or video posts to a community.
  • Every post and comment can be upvoted or downvoted once per user; the totals feed karma on the author's profile.
  • Community pages sort posts by hot, new, top (by time window) and rising.
  • A home feed blends posts from every community the user has joined; logged-out visitors get a popular feed.
  • Threaded comments under each post, sortable and expandable.
  • Moderators remove content, ban users and run automatic rules; anyone can report.
  • Search across posts and communities.
Non-Functional
  • Feed and community pages load in p99 under 250 ms.
  • Read-dominated: well over 95% of requests are page views.
  • Vote totals may trail by a few seconds, but no vote is lost or counted twice.
  • A single viral post must not degrade the rest of the site.
  • New posts appear in "new" within seconds and can climb "hot" within minutes.
  • Availability of 99.95% for reading; posting and voting may degrade first.
Narrow it in the first minutes

This is too big to design completely. Tell the interviewer you will focus on three hard parts: ranking a feed without scanning posts, serving comment trees, and counting votes under contention, and treat media, search and messaging as boxes you can name but not build out.

02

Back-of-envelope numbers

03

Back-of-envelope numbers

Assume 75 million daily active users, about 400,000 communities with activity in a given week, 1.1 million new posts and 9 million comments a day, and 90 million votes a day. A typical user loads about 12 feed or community pages a day.

QuantityWorkingResult
Feed and community page loads75M x 12 per day900M a day, about 10,400 / s
Peak page loads3.5x average in the US eveningabout 36,000 / s
Post detail views with commentsabout 2 per feed pageabout 72,000 / s at peak
Votes90M / 86,400 s, viral bursts 15x1,040 / s average, up to 15,000 / s
New posts1.1M / 86,400 sabout 13 / s
New comments9M / 86,400 sabout 105 / s
Post metadata per year1.1M x 365 x 1.5 KBabout 600 GB
Comments per year9M x 365 x 700 Babout 2.3 TB
Vote records per year90M x 365 x 30 Babout 1 TB
Media uploadsabout 300K images and 40K videos a dayroughly 3 TB a day before transcoding
Hot lists in Redis400K communities x top 1,000 posts x about 40 Babout 16 GB
The takeaway

Writes are tiny next to reads, but votes arrive in violent bursts on a handful of posts. Design the read path around precomputed ranked lists, and the write path around never letting one popular post lock a row.

03

Endpoints

04

Endpoints

GET/api/c/{community}/posts?sort=hot&after=t3_9xk2&limit=25One page of a community listing
{ "posts": [ { "id": "t3_9xk7", "title": "Our city finally got protected bike lanes", "score": 8211, "comments": 644, "age_s": 10380, "my_vote": 0 } ], "after": "t3_9xm1" // opaque cursor, not a page number }
GET/api/home?after=&limit=25Personal feed from joined communities
// same shape as above; logged-out callers get the popular listing instead
POST/api/c/{community}/postsSubmit a post
{ "kind": "image", "title": "Before and after, three years apart", "media_id": "m_51dd0", // uploaded earlier to object storage with a presigned URL "flair": "Infrastructure" } // 201; the post is visible in "new" once the spam check passes
POST/api/things/{thing_id}/voteVote on a post or comment
{ "dir": 1 } // 1, 0 or -1, the desired end state, so a retry changes nothing // 202 Accepted; the total updates asynchronously
GET/api/posts/{post_id}/comments?sort=best&limit=200Comment tree for a post
// a flat node list in display order with depth, plus "more" stubs carrying cursors

04

System architecture

05

System architecture

Main services and data storesDiagram
flowchart LR
	U["Web and apps"] --> CDN["CDN for media and logged-out pages"]
	U --> GW["API gateway"]
	GW --> LS["Listing service"]
	GW --> PS["Post and comment service"]
	GW --> VS["Vote service"]
	LS --> RZ[("Redis: ranked lists per community")]
	PS --> PG[("Postgres, sharded")]
	VS --> VR[("Vote records, Cassandra")]
	VS -. "vote events" .-> K[["Kafka"]]
	PS -. "post and comment events" .-> K
	K --> AG["Score aggregator"]
	AG --> PG
	AG --> RZ
	K --> SR["Search indexer"]
	K --> MOD["Spam and moderation"]

The site is a read path and a write path joined by an event log. On the read side, the listing service answers "give me the next 25 posts for this community and sort" from sorted sets in Redis, then hydrates post ids into full cards from a cache in front of Postgres. Logged-out traffic, which is a large share of views, is served mostly from the CDN with short TTLs.

On the write side, posts and comments go to Postgres through one service, and votes go to their own service with its own store. Neither updates ranked lists directly. Instead they emit events, and the score aggregator applies vote deltas in batches, recomputes hot scores and updates Redis. Search indexing, spam scoring, notifications and karma all hang off the same Kafka topics, so each can fall behind without stopping posts or votes.

Ranked lists

One sorted set per community and sort order, trimmed to the top 1,000 entries. A page read is a range query by rank, a few hundred microseconds in Redis.

Thing store

Postgres sharded by community for posts and by post for comments. Holds titles, bodies, counts and per-sort scores. It is the truth that every cache can be rebuilt from.

Vote store

A wide-column table keyed by user and thing. It answers "did this user vote, and which way" and is the record every count is derived from.

Media pipeline

Clients upload straight to object storage; a worker scans, transcodes and thumbnails, and the CDN serves the results. The application servers never touch the bytes.

05

Deep dive: ranking, trees and vote counting

06

Deep dive: ranking, trees and vote counting

1. Hot ranking without rescoring

"Hot" has to favour posts that are both well liked and recent. A naive formula divides score by age, which means every post's value changes every second and the whole list needs constant rescoring. The trick in Reddit's published ranking code is to make time an additive term that never changes for a given post:

def hot(ups, downs, created_epoch): net = ups - downs order = log10(max(abs(net), 1)) # 10 votes = 1, 100 = 2, 1,000 = 3 sign = (net > 0) - (net < 0) return round(sign * order + (created_epoch - EPOCH) / 45000, 7)

Because the time term uses the creation timestamp, a post's hot value only changes when its votes change. Newer posts automatically start higher, since their timestamp is larger, so older posts sink without anyone touching them. Every 45,000 seconds, about 12.5 hours, of age is worth one order of magnitude of votes: a post from this morning needs roughly ten times the net score to sit level with one made now.

That property is what makes precomputation possible. Store each post in a Redis sorted set for its community with the hot value as the member's score. On each batch of votes, recompute and ZADD it; when a post is created, add it once. A page is a ZREVRANGE by rank. Trim each set to the top 1,000, since nobody pages past that, and rebuild it from Postgres if Redis loses it.

Other sorts are simpler

"New" orders by creation time and never changes. "Top this week" keeps one sorted set per window, scored by net votes, with posts expiring out as they age past the window. "Rising" compares a post's recent vote rate with its community's normal rate, and is computed by a periodic job over the last few hours rather than per vote.

2. The home feed: merge at read time

A user who joined 60 communities needs one blended list. Writing every new post into every member's feed would mean fan-out to millions of members for big communities, and posts change rank constantly after they are written. So the home feed is pulled, not pushed: read the top 50 from each joined community's hot set in one pipelined Redis call, merge them by hot value with a heap, apply per-community caps so one busy community cannot fill the page, and cache the merged result for that user for about a minute. Users who joined thousands of communities get a sampled subset per request, which nobody notices.

3. Comment trees

Each comment row stores its parent id and a materialised path such as 0f3.1a9.22c, built from the parent's path plus its own id. Comments never change parent, so paths never need rewriting. Two queries cover everything a page needs: "the best k children of this comment", served by an index on post, parent and sort score, and "this comment and everything under it", served by a prefix match on path.

The first page of a thread is built breadth first with a node budget: the best 40 top-level comments, the best few replies under each, fewer again at the next level, until roughly 200 nodes are chosen. Anything cut off becomes a "load more" stub. "Best" uses the lower bound of a Wilson confidence interval on the upvote share, so a comment with 5 up and 0 down does not outrank one with 900 up and 100 down. That first page is cached per post and sort, and the reader's own votes are merged in afterwards.

4. The vote pipeline

From a click to an updated rankingDiagram
flowchart LR
	C["Vote request"] --> V["Vote service"]
	V --> R[("Vote record: user, thing, dir")]
	R --> D["Delta: new dir minus old dir"]
	D --> K[["Kafka, keyed by thing id"]]
	K --> A["Aggregator sums deltas per 2 s window"]
	A --> P[("Postgres: one update per thing per window")]
	A --> Z[("Redis: new hot value")]
	A --> KM["Karma updater, async"]

The vote service does one strongly ordered thing: it reads the user's existing vote on that thing, writes the new one, and emits the difference. Changing an upvote to a downvote emits minus two for ups minus downs; repeating the same vote emits nothing. That makes double clicks and retries harmless before anything is counted.

Events are partitioned by thing id, so all deltas for one post reach one aggregator instance in order. It keeps running sums in memory and flushes every two seconds, issuing a single update per post no matter whether it received 3 votes or 30,000 in that window. It then recomputes the hot value and writes it to Redis. Offsets are committed only after the flush, and each flush carries a window id so a replay after a crash cannot apply the same window twice.

Why counts can always be repaired

The vote records are the ledger; the counters are a cache of it. If a counter is ever wrong, a job can recount from the records for that thing and overwrite it. Designs that keep only counters have nothing to repair from.

06

Common mistakes

⚠

Common mistakes

1
Sorting the community's posts with SQL on every page view

An ORDER BY over a computed hot expression must read every candidate post, and it runs tens of thousands of times a second at peak. The database spends its life recomputing a list that barely changed.

Instead: keep ranked lists in sorted sets and update them when votes land, so a page is a range read.
2
Pushing every post into every member's home feed

Large communities have tens of millions of members, so one post becomes tens of millions of writes, and those precomputed entries go stale as soon as the post's rank moves.

Instead: merge the joined communities' ranked lists at read time and cache the result briefly per user.
3
Incrementing a counter column on every vote

A post receiving thousands of votes per second turns one row into a lock queue. Latency for every write on that shard climbs with it.

Instead: record votes per user, stream deltas, and apply them in batched windows.
4
Fetching comment children one parent at a time

A 300-comment page becomes 300 queries, each with its own round trip, and the deepest branch decides the latency.

Instead: fetch one level per query for all parents at once, with a total node budget.
5
Ranking "best" comments by raw net score

Early comments collect votes simply by being seen first, and a small sample with no downvotes looks better than a large, mostly positive one.

Instead: rank by a confidence bound on the upvote share, which rewards evidence rather than luck.

07

Decisions and their costs

07

Decisions and their costs

Chosen
Precomputed ranked lists

Pages read in microseconds and the database is off the hot path. A post's new position appears a couple of seconds after the votes that moved it, which nobody perceives.

Fast reads, tiny staleness
Rejected
Rank on demand

Always exact, but it costs a scan per view. Acceptable only for low-traffic sorts such as a moderator's queue.

Too costly per view
Chosen
Pull-based home feed

No fan-out on post, rankings are always current, and adding a big community costs nothing extra. The price is a merge on each uncached request, roughly 60 small Redis reads pipelined into one round trip.

Fits community-based feeds
Alternative
Push-based feed inboxes

Works well for follow graphs where most accounts have few followers. Here membership is extremely skewed and rank changes after posting, so the inboxes would be both huge and stale.

Better for follow graphs
Chosen
Eventually consistent counts, exact vote records

The user's own vote is read straight from the vote store, so their arrow is always right. The public number catches up within seconds, and displayed totals can be lightly fuzzed to frustrate manipulation.

Correct where users look
Rejected
Synchronous exact totals

Buys precision nobody can see, at the cost of hot-row locking on exactly the posts with the most traffic.

Wrong place for strictness
Chosen
Postgres for things, wide-column store for votes

Posts and comments need transactions, secondary indexes and moderation queries, which suit Postgres. Votes are a huge, simple key-value workload that suits Cassandra or a similar store.

Store per access pattern
Cost
Two systems to run

Operating two databases and keeping their data consistent through events is real work. A small team could start with votes in Postgres and split later.

Split when volume demands

08

Failure modes

08

Failure modes

🤖
A bot network pushes a post up

Thousands of aged accounts upvote one link within ten minutes, putting a scam on the front page of a large community.

Weight every vote by a trust score from account age, behaviour and network signals before it reaches the aggregator, detect bursts from correlated accounts, and keep the raw records so votes can be voided and recounted after the fact.
🔥
A viral post stampedes the caches

A post reaches the top of the popular feed. Its card and its comment page are requested tens of thousands of times a second, and each cache expiry sends a wave of identical rebuilds to one Postgres shard.

Refresh hot entries in the background before they expire, collapse concurrent misses into one rebuild, put logged-out views behind the CDN, and add read replicas for the shard that owns the post.
🌲
A comment tree grows absurdly deep

A reply chain reaches thousands of levels. Paths become very long and renderers on older phones crash.

Limit depth per page with "continue this thread" stubs, cap path length by encoding ids compactly, and rate-limit replies within a single chain.
📈
Karma farming

Accounts repost old popular content to build karma, then sell themselves to spammers who need trusted-looking accounts.

Detect reposts with image hashes and link history, compute karma asynchronously so it can be adjusted, and avoid using karma alone as a trust signal anywhere it matters.
🧮
Counters drift after an aggregator crash

An aggregator dies after writing a flush to Postgres but before committing its Kafka offset. On restart it replays the window and some posts gain votes twice.

Tag each flush with its window id and partition, store the last applied id per thing, and skip windows already applied. Run a nightly recount from vote records for the most active things as a safety net.
🔎
Search lags during breaking news

A major event triples posting volume and the search indexer falls an hour behind, so people searching for the event find nothing new.

Scale indexer consumers on lag, prioritise new documents over score updates in the indexing queue, and fall back to the community's "new" listing for very recent results.

09

How to handle the interview

09

How to handle the interview

1

Pick your three hard parts and say so. Ranking, comment trees and vote counting are where the depth is. Scoping out media and messaging early buys you time for the parts that score.

2

Explain why the hot formula allows precomputation. The key insight is that age enters as a fixed term per post, so scores only change on votes. Write the formula and show what 12.5 hours is worth.

3

Argue pull versus push for the home feed. Skewed community sizes and shifting ranks make pull the better fit here; contrast it with a follow-based feed to show you know when the answer flips.

4

Walk a vote from click to rank. Record, delta, partitioned stream, batched flush, new hot value. Point out where retries and crashes are made harmless.

5

Give the comment tree a budget. Per-level limits, stubs and a confidence-based "best" sort cover most follow-up questions.

6

Raise manipulation yourself. Vote trust, fuzzed counts and repairable ledgers show you have thought about adversaries, which any voting system attracts.

7

Separate logged-in and logged-out traffic. Anonymous pages are identical for everyone, so a CDN can carry a large share of total load cheaply.

10

How it grows

11

How it grows

Stage 1: a single community site
One app, one database, sort in SQL

Posts, comments and votes live in Postgres and pages sort with queries. At a few thousand users this is fast enough and far simpler to change.

Stage 2: thousands of communities
Cached ranked lists

Hot scores move into sorted sets updated on vote, rendered pages are cached, and logged-out traffic shifts to a CDN.

Stage 3: millions of daily users
Vote pipeline and event log

Votes get their own service and store, deltas flow through Kafka, and an aggregator batches updates. Search, notifications and spam detection subscribe to the same events.

Stage 4: tens of millions of daily users
Sharding and media pipeline

Postgres shards by community and post, uploads go directly to object storage, and transcoding runs in its own worker fleet.

Stage 5: a mature platform
Personalised ranking on top

The home feed adds a learned ranker over the merged candidates, trust scoring weights every vote, and services split further along team lines.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.