Redis

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