medium Designing Reddit
The two correctness-critical pieces
A Reddit-style backend is read-heavy and rank-driven, so two decisions carry most of the design's weight and are the easiest to get subtly wrong: how a post's ranking score is computed, and how a post reaches every subscriber's feed. This rewrite traces both end-to-end with worked numbers, and fixes a bug in the ranking formula carried by the original page.
The original stated Reddit's "hot" score as log10(|ups - downs|) + sign(v) · (post_age_in_seconds / 45000). That is wrong in two ways. First, it adds a positive multiple of post_age, so an older post scores higher — the opposite of keeping a feed fresh. Second, post_age = now - created is measured from the current clock, so every post's score changes on every read and the ordering is never stable. Reddit's real algorithm instead uses the post's absolute creation time as the time term, computed once at submission. The corrected formula and a full trace are in the ranking section below.
Write flow, traced: creating a post
User A submits a link post to r/systems. The write path never blocks on fan-out:
- Gateway.
POST /postshits the API gateway, which authenticates A's token and routes to the Post Service. - Persist. The Post Service mints a globally unique
post_id(Snowflake), then writes one row to the sharded posts store — partitioned so a subreddit's posts co-locate, e.g. primary key(subreddit_id, post_id)in Cassandra. Denormalized fields (score,comment_count, author username) are stored inline to avoid cross-shard joins on read. - Initial rank. Because the time term depends only on
created_utc(see below), the Post Service computes the post'shotscore right here, once, and stores it. It will never need recomputing as the clock advances. - Enqueue fan-out. The service does not synchronously write into subscribers' feeds. It publishes a
PostCreated{post_id, subreddit_id, hot}job to a fan-out queue and returns201 Createdto A in a few milliseconds. - Async materialize. Feed workers consume the job and, for push-eligible subreddits, insert
(post_id, hot)into each subscriber's feed sorted set. This work happens off the request path, so a slow fan-out never slows the post button.
Comment threading, traced
Comments form arbitrarily deep trees, so we store them as an adjacency list: each comment row carries a parent_comment_id (NULL for a top-level comment) and the owning post_id. Crucially, comments are sharded by post_id, so one post's entire thread lives on one partition — the tree is fetched without scatter-gather.
Tracing a reply and a thread read:
- Reply. User B replies to comment C42 on post P.
POST /posts/P/commentswithparent_comment_id = 42writes a row keyed to P's partition, bumps the denormalizedcomment_counton the post, and publishesCommentAdded{post_id:P, parent:42, author:B}for the notification service. - Read the tree.
GET /posts/P/commentsissues a single partition read for all of P's comments, then rebuilds the tree in memory by linking each comment to its parent. Ordering within a sibling group reuses the same score-based rule the feed uses (top replies first), or chronological for "new". - Hot threads. A post with tens of thousands of comments still reads from one partition; the top-level comments are cached separately so the common "first screen" render avoids materializing the whole tree.
Ranking: the corrected "hot" formula
The flagged bug is here. Reddit ranks a post by its net votes and its age, but the time term is the post's absolute creation timestamp, not an elapsed age. The published algorithm:
s = ups - downs
order = log10(max(|s|, 1))
sign = +1 if s > 0 else (-1 if s < 0 else 0)
t = created_utc_seconds - 1134028003 # seconds since Reddit's epoch anchor (Dec 8 2005)
hot = round(sign * order + t / 45000, 7)Read t carefully: it is seconds-since-epoch of the post's creation. A newer post has a larger t and therefore a higher hot score. Two consequences drive the whole design:
- Freshness without decay. Old posts do not lose points over time — the score has no
nowin it. Newer posts simply start higher. That is what keeps the feed fresh. - Computed once, forever stable. Because
tis fixed at submission and votes only nudge the tinyorderterm, a post'shotis computed at creation and updated only when votes change — never on a timer, never per read. That is why the write flow above can rank the post inline. - 45000 s ≈ 12.5 h. Every 12.5 hours of newer creation adds exactly
+1to the time term. Sinceorderis a base-10 log,+1equals one order of magnitude — 10× the net votes. So a post made 12.5 h later needs only one-tenth the votes to tie.
Why the original was wrong: it used post_age = now - created with a + sign, which (a) makes older posts rank higher, backwards from the intent, and (b) makes every score a function of the current clock, forcing a full recompute of every post on every read and yielding an ordering that shifts continuously. Swapping the relative post_age for the absolute created_utc term fixes both.
Worked example: computing a hot score
Take Post P: 300 upvotes, 100 downvotes, created at created_utc = 1,780,000,000 (late May 2026).
s = 300 - 100 = 200order = log10(max(200,1)) = log10(200) = 2.3010300sign = +1(s > 0)t = 1,780,000,000 - 1,134,028,003 = 645,971,997t / 45000 = 14,354.9332667hot = round(1 × 2.3010300 + 14,354.9332667, 7) = 14357.2342967
Notice the time term (14,354.9) dwarfs the vote term (2.3). That is expected and fine: all posts being ranked share a nearly identical time magnitude, so what decides order at the margin is the difference in creation time plus the vote term.
Now Post Q — identical votes (300/100) but created 12.5 h later, created_utc = 1,780,045,000:
t = 645,971,997 + 45,000 = 646,016,997, sot/45000 = 14,355.9332667hot = 2.3010300 + 14,355.9332667 = 14358.2342967hot(Q) - hot(P) = 1.0000000— Q wins by exactly one time-unit.
For the older Post P to tie Q, its order must rise by 1, i.e. log10(s_P) = 3.30103, so s_P = 10^{3.30103} = 2000. P needs a net 2000 votes to match Q's 200 — the promised 10×. This single arithmetic fact is the whole judgment layer of the ranking: a post has roughly a 12.5-hour window to gather each additional order of magnitude of votes before newer content structurally out-ranks it.
Feed delivery: push vs pull, with costs
Delivering feeds is the second correctness-critical choice. Two pure strategies sit at opposite ends of the write/read trade-off:
| Model | Write cost (per post) | Read cost (per feed load) | Best when | Worst when |
|---|---|---|---|---|
| Push (fan-out on write) | O(subscribers) feed writes | O(1) — read a pre-merged list | read-heavy, modest fan-out, active readers | million-subscriber producers (write storm); inactive readers (materialized work no one reads) |
| Pull (fan-out on read) | O(1) — one row write | O(sources) query + merge every load | huge fan-out; rarely-reading users | latency-sensitive default feed; users following many sources |
Push makes reads trivial but pays a write proportional to the audience; pull makes writes trivial but pays a merge on every read. Neither alone survives Reddit's shape, where most communities are small but a handful (r/all, r/pics) have tens of millions of subscribers.
The hybrid decision rule and the celebrity fallback
The design is neither pure push nor pure pull — it is a hybrid keyed on a producer's fan-out. Let F be a subreddit's subscriber count (or, for user-follows, a user's follower count) and T a tuned threshold (order of tens of thousands to a low millions).
The rule:
- If
F ≤ T→ push. On a new post, feed workers insert(post_id, hot)into each subscriber's sorted-set feed. Reads areO(1). - If
F > T→ mark the producer pull-only. Its posts are not fanned out. They stay in the per-subreddit sorted set and are fetched live. - At read time, merge. The Feed Service reads the user's materialized (pushed) feed, then live-pulls the top-K recent posts from just the handful of pull-only sources that specific user follows, and merges everything by
hot. A user follows only a few mega-communities, so the merge fan-in stays small.
Why this beats either pure model — the celebrity problem. A post to r/pics with 30M subscribers under pure push would trigger 30M feed writes, in a burst, most landing in feeds no one will read soon. Marking r/pics pull-only makes that post a single write, pulled on demand only by users who actually load a feed containing it. Pure pull, meanwhile, would force even a user's small-subreddit posts to be re-merged on every read; pushing those keeps the common case O(1).
When to use push / when not. Use push for the common case: modest fan-out, read-heavy, active readers who load feeds often (materialized work is amortized over many reads). Do not push for producers above the threshold (write amplification / bursts) or for dormant readers whose feeds would be materialized and never read — a last-active heuristic can suppress fan-out to inactive users and let them pull on return.
When to use pull / when not. Use pull for very-high-fan-out producers and for the long tail of inactive users. Do not make pull the default for active users' primary feed: an O(sources) merge on every load blows the low-latency (P99 < ~100 ms) target once a user follows many communities. The hybrid resolves this by pushing the many small sources and pulling only the few huge ones.
Sources
- Amir Salihefendic, "How Reddit ranking algorithms work" — derivation of the
hotformula, the 45000-second (12.5 h) constant, and the 1134028003 epoch anchor. - Reddit's open-sourced ranking code (
_sorts.pyx/hot()) — canonical reference forsign · order + seconds/45000using absolute creation time. - Grokking the System Design Interview — "Designing Reddit / Facebook Newsfeed": fan-out-on-write vs fan-out-on-read and the hybrid celebrity fallback.
- Original lesson page, corrected: the ranking formula's
post_ageterm is replaced with the absolutecreated_utcterm, and the truncated feed section is completed with the hybrid decision rule and pull fallback.
🤖 Don't fully get this? Learn it with Claude
Stuck on Designing Reddit? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Progressively stronger hints — you still solve it.
I'm working on the problem **Designing Reddit** (System Design). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
See the technique, not just code.
Explain the optimal approach to **Designing Reddit** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Catch bugs, edge cases, sub-optimality.
I'll paste my solution to **Designing Reddit**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Lock in recognition with look-alikes.
Give me 2 problems that use the SAME underlying pattern as **Designing Reddit**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.