Building a Social Network Timeline with Redis List Data Type
June 9, 2026
7 min read
A social timeline is one of the most read-heavy features in any application. When a user loads their feed, you need the latest posts from everyone they follow — instantly. Redis Lists with fan-out on write make this possible: each read is a single O(N) list slice, no joins, no sorting.
The Fan-Out on Write Pattern
When a user posts, the system immediately pushes the post ID to the timeline list of every follower. Each user's timeline is a pre-computed, sorted list of post IDs. Loading the feed becomes a single LRANGE call.
# User alice (id:1) posts something (post id: 99)
# She has 3 followers: bob (id:2), carol (id:3), dave (id:4)
# Push post to each follower's timeline
LPUSH timeline:user:2 99
LPUSH timeline:user:3 99
LPUSH timeline:user:4 99
# Cap timelines at 1000 items (trim old posts)
LTRIM timeline:user:2 0 999
LTRIM timeline:user:3 0 999
LTRIM timeline:user:4 0 999
Reading the Timeline
# Load first page of bob's timeline (newest 20 posts)
LRANGE timeline:user:2 0 19
# Load second page
LRANGE timeline:user:2 20 39
# Count total items in timeline
LLEN timeline:user:2
Python Implementation
import redis
import json
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
MAX_TIMELINE_SIZE = 1000
def get_followers(user_id):
# Return follower IDs from your database
return db.query("SELECT follower_id FROM follows WHERE followed_id = %s", user_id)
def publish_post(author_id, post_id):
followers = get_followers(author_id)
pipe = r.pipeline()
for follower_id in followers:
timeline_key = f"timeline:{follower_id}"
pipe.lpush(timeline_key, post_id)
pipe.ltrim(timeline_key, 0, MAX_TIMELINE_SIZE - 1)
# Also add to author's own timeline
pipe.lpush(f"timeline:{author_id}", post_id)
pipe.ltrim(f"timeline:{author_id}", 0, MAX_TIMELINE_SIZE - 1)
pipe.execute()
def get_timeline(user_id, page=1, page_size=20):
start = (page - 1) * page_size
end = start + page_size - 1
post_ids = r.lrange(f"timeline:{user_id}", start, end)
# Batch-fetch post details from cache or DB
posts = []
for post_id in post_ids:
post = r.hgetall(f"post:{post_id}")
if post:
posts.append(post)
return posts
Storing Post Metadata
def create_post(author_id, content):
post_id = generate_id()
post_key = f"post:{post_id}"
r.hset(post_key, mapping={
"id": post_id,
"author_id": author_id,
"content": content,
"created_at": datetime.utcnow().isoformat(),
"likes": 0
})
publish_post(author_id, post_id)
return post_id
Fan-Out on Write vs Fan-Out on Read
- Fan-out on write (this approach) — writes are more expensive (push to N followers), reads are instant. Best for users with moderate follower counts.
- Fan-out on read — write once, merge feeds at read time. Cheaper writes, expensive reads (merge N author timelines per request). Best for "celebrity" accounts with millions of followers.
Most production systems use a hybrid approach: fan-out on write for regular users, fan-out on read for high-follower accounts (e.g., accounts with 10k+ followers don't get pushed into every follower's list directly).
Key Takeaways
- LPUSH prepends the newest post ID to each follower's timeline list
- LTRIM caps the list size after every push — keeps memory bounded
- LRANGE reads a page of the timeline — one command, no sorting needed
- Store post IDs in the timeline, fetch full content from a separate hash or database
- For celebrity accounts, use fan-out on read to avoid massive write amplification