0Pricing
DSA Interview Prep · Lección

Diseñar un limitador de solicitudes y un feed de Twitter

Aplique el marco a dos problemas de diseño canónicos: limitación de solicitudes con token bucket o ventana deslizante, y un feed de noticias con fan-out-on-write frente a fan-out-on-read.

Diseñar un limitador de solicitudes y un feed de Twitter es una lección gratuita de DSA Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DSA Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DSA Interview Prep incluye 4 lecciones en total.

Por qué la limitación de solicitudes es esencial

La limitación de solicitudes controla el número de solicitudes que un cliente puede realizar a una API en una ventana de tiempo determinada. Sin ella, un único cliente que se comporte mal (o un ataque DDoS) puede saturar los recursos del servidor y degradar el servicio para todos los usuarios. La limitación de solicitudes también protege contra ataques de fuerza bruta, evita la extracción automatizada de datos de la API y garantiza un uso equitativo de los recursos compartidos.

Niveles habituales de limitación: por ID de usuario, por clave de API, por dirección IP, por endpoint o mediante una combinación de estos criterios. Límites habituales: 100 solicitudes por minuto por usuario, 1000 por hora por clave de API. El limitador de solicitudes debe ser rápido (añadir <1ms de sobrecarga) y distribuido (consistente en todas las réplicas del servidor de API).

# Rate limiting scenarios
use_cases = [
    ('API authentication endpoint', '5 attempts per 15 min per IP', 'Brute-force protection'),
    ('Public search API',           '100 requests per minute per key', 'Fair use enforcement'),
    ('Email sending',               '50 emails per hour per user', 'Spam prevention'),
    ('Payment processing',          '10 transactions per second per account', 'Fraud prevention'),
    ('File upload',                 '5 uploads per minute per user', 'Resource quota'),
    ('Notification service',        '1000 pushes per second globally', 'Cost control'),
]
print(f'{'Endpoint/Feature':35s} {'Limit':40s} {'Reason'}')
print('-'*95)
for endpoint, limit, reason in use_cases:
    print(f'{endpoint:35s} {limit:40s} {reason}')

Algoritmo de limitación de solicitudes 1: Token Bucket

El algoritmo token bucket mantiene un depósito con una capacidad máxima de N tokens. Los tokens se añaden a una tasa fija (por ejemplo, 10 por segundo). Cada solicitud consume un token. Si el depósito está vacío, la solicitud se rechaza. Si está por debajo de su capacidad, la solicitud se acepta y se consume el token.

Token bucket permite las ráfagas: si no llegan solicitudes durante 5 segundos, el depósito se llena hasta alcanzar N tokens y, a continuación, pueden llegar N solicitudes inmediatamente. Esto resulta adecuado para las API en las que se aceptan ráfagas ocasionales. Los dos parámetros son la capacidad (tamaño de la ráfaga) y la tasa de reposición.

import time

class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity       # max tokens (burst size)
        self.refill_rate = refill_rate # tokens added per second
        self.tokens = capacity         # start full
        self.last_refill = time.time()

    def allow(self):
        now = time.time()
        elapsed = now - self.last_refill
        # Refill tokens based on elapsed time
        self.tokens = min(self.capacity,
                          self.tokens + elapsed * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True    # request allowed
        return False       # rate limited

bucket = TokenBucket(capacity=5, refill_rate=2)  # 2 tokens/sec, burst=5
for i in range(8):
    allowed = bucket.allow()
    print(f'Request {i+1}: {"ALLOWED" if allowed else "REJECTED"} (tokens={bucket.tokens:.1f})')
    time.sleep(0.1)  # 0.1s between requests

Algoritmo de limitación de solicitudes 2: registro de ventana deslizante

El registro de ventana deslizante almacena una marca de tiempo por cada solicitud en un conjunto ordenado. Para cada solicitud nueva, elimina las marcas de tiempo anteriores al inicio de la ventana y comprueba si el número de marcas restantes está por debajo del límite. Si es así, añade la marca de tiempo actual y permite la solicitud; de lo contrario, la rechaza.

Este método es preciso: cuenta exactamente cuántas solicitudes se produjeron durante los últimos N segundos. La desventaja es el alto uso de memoria (una entrada por solicitud y por usuario). Con un límite de 1000 solicitudes por minuto para 100K usuarios, el peor caso es de 100M de entradas de registro. No resulta adecuado para un tráfico muy elevado a menos que se combine con sharding.

import time
from collections import deque

class SlidingWindowLog:
    def __init__(self, limit, window_seconds):
        self.limit = limit
        self.window = window_seconds
        self.logs = {}     # user_id -> deque of timestamps

    def allow(self, user_id):
        now = time.time()
        if user_id not in self.logs:
            self.logs[user_id] = deque()
        log = self.logs[user_id]
        window_start = now - self.window
        # Remove expired timestamps
        while log and log[0] <= window_start:
            log.popleft()
        # Check limit
        if len(log) < self.limit:
            log.append(now)
            return True
        return False

limiter = SlidingWindowLog(limit=3, window_seconds=10)
for i in range(5):
    allowed = limiter.allow('user123')
    print(f'Request {i+1}: {"ALLOWED" if allowed else "REJECTED"}')
    time.sleep(0.5)

Algoritmo de limitación de solicitudes 3: contador de ventana deslizante

El contador de ventana deslizante aproxima la ventana deslizante mediante dos depósitos — el minuto actual y el minuto anterior — ponderados según cuánto haya avanzado el minuto actual. Esto reduce la memoria de O(solicitudes) a O(1) por usuario, al tiempo que aproxima con bastante precisión el recuento exacto de la ventana deslizante.

Fórmula: estimated_count = prev_count × (1 - fraction_of_window_elapsed) + curr_count. Si este recuento estimado supera el límite, se rechaza la solicitud. Este es el algoritmo que utilizan Cloudflare y Kong a gran escala debido a su uso de memoria O(1) por usuario y su gran precisión.

import time
import math

class SlidingWindowCounter:
    def __init__(self, limit, window_seconds=60):
        self.limit = limit
        self.window = window_seconds
        self.buckets = {}   # user_id -> {prev_count, curr_count, curr_window_start}

    def allow(self, user_id):
        now = time.time()
        window_start = int(now // self.window) * self.window

        if user_id not in self.buckets or self.buckets[user_id]['window'] < window_start - self.window:
            self.buckets[user_id] = {'prev': 0, 'curr': 0, 'window': window_start}
        elif self.buckets[user_id]['window'] < window_start:
            self.buckets[user_id] = {'prev': self.buckets[user_id]['curr'], 'curr': 0, 'window': window_start}

        b = self.buckets[user_id]
        fraction = (now - window_start) / self.window
        estimated = b['prev'] * (1 - fraction) + b['curr']

        if estimated < self.limit:
            b['curr'] += 1
            return True
        return False

limiter = SlidingWindowCounter(limit=5, window_seconds=10)
for i in range(7):
    print(f'Request {i+1}: {"OK" if limiter.allow("user1") else "RATE LIMITED"}')
    time.sleep(0.3)

Limitación de solicitudes distribuida con Redis

En un sistema distribuido con varios servidores de aplicaciones, la limitación de solicitudes debe ser centralizada; de lo contrario, cada servidor lleva su propio recuento y los límites se multiplican efectivamente por el número de servidores. Redis, con sus operaciones atómicas, es la solución estándar: utilice INCR y EXPIRE para un contador de ventana fija, o ZADD y ZCOUNT para un registro de ventana deslizante.

El enfoque basado en scripts de Lua permite que varias operaciones de Redis sean atómicas, evitando condiciones de carrera en las que dos servidores incrementan el contador simultáneamente cuando está justo por debajo del límite. Redis procesa los scripts de Lua como un único comando, lo que garantiza la atomicidad sin bloqueos distribuidos.

# Distributed rate limiting with Redis (pseudocode / simulation)

# Fixed window counter using Redis INCR + EXPIRE
def redis_fixed_window(redis_client, user_id, limit, window_sec):
    key = f'rl:{user_id}:{int(time.time() // window_sec)}'
    count = redis_client.incr(key)          # atomic increment
    if count == 1:
        redis_client.expire(key, window_sec)  # set TTL on first request
    return count <= limit

# Sliding window with sorted set
def redis_sliding_window(redis_client, user_id, limit, window_sec):
    now = time.time()
    key = f'rl:{user_id}'
    # Remove old entries, count recent, add current
    # Atomic with Lua: multi-step operation
    lua_script = '''
    local key = KEYS[1]
    local now = ARGV[1]
    local window = ARGV[2]
    local limit = ARGV[3]
    redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
    local count = redis.call('ZCARD', key)
    if count < tonumber(limit) then
        redis.call('ZADD', key, now, now)
        redis.call('EXPIRE', key, window)
        return 1  -- allowed
    end
    return 0  -- rejected
    '''
    print('Redis Lua script ensures atomicity across ZREM + ZCARD + ZADD')

Diseño del feed de Twitter: requisitos

Diseñemos un sistema de feed de noticias similar a Twitter. Requisitos funcionales: los usuarios pueden publicar tuits (de hasta 280 caracteres), seguir a otros usuarios y consultar un feed de tuits de las personas a las que siguen, ordenado por fecha de publicación más reciente. Requisitos no funcionales: 300M de usuarios activos diarios, 500M de tuits al día, el feed debe cargarse en <2 segundos y la proporción lectura:escritura es de aproximadamente 100:1.

Estimaciones de capacidad: 500M de tuits/día ÷ 86400 ≈ 5800 tuits/segundo. Las lecturas son ≈ 580K/segundo. Cada tuit ocupa ≈ 300 bytes; 500M × 300B = 150 GB/día de almacenamiento nuevo de tuits. La agregación del feed es el principal desafío de ingeniería.

# Twitter feed requirements and estimates
reqs = {
    'Functional': [
        'Post tweet (text, image, video)',
        'Follow/unfollow users',
        'View home feed (tweets from followees, newest first)',
        'View user timeline (all tweets by one user)',
        'Like and retweet',
        'Search tweets (basic keyword)',
    ],
    'Non-functional': [
        '300M DAU, 500M tweets/day => 5800 writes/sec',
        '100:1 read:write => 580K feed reads/sec',
        'Feed load < 2 seconds (p95)',
        '99.99% availability',
        'Tweets retained indefinitely (tweets never deleted by default)',
    ],
    'Estimates': [
        'Storage: 500M tweets * 300B = 150 GB/day, 54 TB/year',
        'Media: separate object store (S3), CDN-served',
        'Feed cache: 300M users * top-100-tweets * 100B = 3 TB (hot feeds in Redis)',
    ],
}
for category, items in reqs.items():
    print(f'{category}:')
    for item in items: print(f'  - {item}')
    print()

Fan-out en escritura: feeds precalculados

En el fan-out on write, cuando el usuario A publica un tuit, el sistema lo distribuye inmediatamente al feed de cada seguidor. Cuando un seguidor solicita su feed, este ya está precalculado y almacenado en Redis: basta con leer una lista de Redis, con un coste O(k), donde k es el tamaño del feed (normalmente limitado a 1000 tuits).

El desafío son las celebridades con millones de seguidores, que generan operaciones de fan-out masivas. La publicación de un tuit por parte de Justin Bieber requiere escribirlo simultáneamente en los feeds de más de 100M de seguidores: un problema real al que se enfrentó Twitter y que denominó el «problema de las celebridades». El servicio de fan-out de escritura debe ser asíncrono y estar basado en colas para gestionar estos picos.

# Fan-out on write (push model)
fan_out_steps = [
    '1. User posts tweet => write to tweets table (source of truth)',
    '2. Publish event to message queue (Kafka topic: tweet-created)',
    '3. Fan-out workers consume from queue:',
    '   a. Fetch list of followers from follows table',
    '   b. For each follower: LPUSH feed:{follower_id} tweet_id',
    '   c. Trim feed to last 1000 tweets: LTRIM feed:{follower_id} 0 999',
    '4. Feed read: LRANGE feed:{user_id} 0 99 => hydrate tweet_ids => response',
]
for step in fan_out_steps:
    print(step)

print('\nPros:')
print('  - Feed reads are O(1): just read from Redis list')
print('  - Feed is always sorted by recency automatically')
print('\nCons:')
print('  - Celebrities with 100M followers => 100M Redis writes per tweet')
print('  - Fan-out lag: followers may see tweet 10-30 seconds late at peak')
print('  - Inactive users waste Redis storage for precomputed feeds')

Fan-out híbrido: resolver el problema de las celebridades

El enfoque híbrido combina el fan-out on write para los usuarios habituales y el fan-out on read para las celebridades. Un usuario se clasifica como celebridad si su número de seguidores supera un umbral (por ejemplo, 1 millón de seguidores). Para los usuarios habituales, los tuits se insertan en todos los feeds de sus seguidores en el momento de la publicación. En el caso de las celebridades, sus tuits NO se insertan; en su lugar, cuando un seguidor consulta su feed, el sistema recupera los tuits recientes de la celebridad y los combina con el feed precalculado.

Este modelo híbrido se aproxima al que Twitter utiliza en realidad. La etapa de combinación es rápida porque las celebridades publican pocas veces y la combinación tiene un coste O(f), donde f es el número de cuentas de celebridades que sigue el usuario (normalmente, un número reducido).

# Hybrid fan-out implementation sketch
CELEBRITY_THRESHOLD = 1_000_000   # followers > 1M => celebrity

def on_post_tweet(user_id, tweet_id, follower_count):
    if follower_count <= CELEBRITY_THRESHOLD:
        # Fan-out to all followers (async via Kafka)
        print(f'User {user_id}: fan-out tweet {tweet_id} to {follower_count} followers')
        # => queue to fan-out workers
    else:
        print(f'Celebrity {user_id}: tweet {tweet_id} stored in timeline only')
        # => only write to tweets table + user timeline
        # => followers get it on demand when reading feed

def get_home_feed(user_id, followees):
    # 1. Get precomputed feed (fan-out on write tweets)
    precomputed = f'LRANGE feed:{user_id} 0 499'   # up to 500 tweets

    # 2. Find celebrity followees
    celebrity_followees = [u for u in followees if is_celebrity(u)]

    # 3. Fetch recent tweets from celebrities (fan-out on read)
    celebrity_tweets = []
    for celeb in celebrity_followees:
        tweets = f'GET tweets WHERE user_id={celeb} ORDER BY created_at DESC LIMIT 20'
        celebrity_tweets.extend(tweets)

    # 4. Merge and sort by recency
    combined = merge_and_sort(precomputed, celebrity_tweets)
    return combined[:100]

print('on_post_tweet for regular user:')
on_post_tweet('user123', 'tweet_abc', 500)
print('on_post_tweet for celebrity:')
on_post_tweet('celebrity456', 'tweet_xyz', 50_000_000)

Feed de Twitter: arquitectura completa

La arquitectura completa del feed de Twitter combina varios sistemas:

  • Servicio de tuits: escribe los tuits en Cassandra (alto rendimiento de escritura, datos de series temporales)
  • Servicio de fan-out: workers asíncronos (consumidores de Kafka) que insertan los identificadores de los tuits en los feeds de los seguidores en Redis
  • Servicio de feed: lee el feed de Redis, completa los identificadores de los tuits con los objetos completos y combina los tuits de las celebridades
  • Servicio de seguimiento: administra el grafo social (quién sigue a quién) en una base de datos de grafos o SQL fragmentado
  • Servicio de cronología: proporciona los tuits propios de un usuario (separado del feed principal)
# Twitter architecture summary
architecture = '''
[User] --> [API Gateway + Load Balancer]
                |
    +-----------+-----------+
    |           |           |
[Tweet Svc] [Feed Svc]  [Follow Svc]
    |           |           |
[Cassandra] [Redis Feeds] [Graph DB]
    |           |
[Kafka] <-- [Fan-out
    |        Workers]
[S3 + CDN]  (tweet_ids
(media)      => follower
             feed lists)

Key design choices:
- Tweets stored in Cassandra (PRIMARY KEY (user_id, created_at))
- Feed stored in Redis as list of tweet_ids per user (LPUSH/LTRIM/LRANGE)
- Fan-out via Kafka + workers (decoupled, retryable, scalable)
- Hybrid: regular users = push; celebrities = pull-on-read
- Hydration: tweet_ids -> full tweet objects via Cassandra read
'''
print(architecture)

Encabezados y respuestas de error del limitador de solicitudes

Un limitador de solicitudes bien diseñado comunica sus límites a los clientes mediante encabezados de respuesta HTTP. Esto permite a los clientes implementar lógica de reintento y a los paneles mostrar el uso. Encabezados estándar:

  • X-RateLimit-Limit: número máximo de solicitudes permitidas en la ventana
  • X-RateLimit-Remaining: solicitudes restantes en la ventana actual
  • X-RateLimit-Reset: marca de tiempo Unix en la que se restablece la ventana
  • Retry-After: segundos que se deben esperar antes de reintentar (en una respuesta 429)

El código de estado HTTP para las respuestas limitadas es 429 Too Many Requests.

# Rate limit response headers
def build_rate_limit_headers(limit, remaining, reset_timestamp, retry_after=None):
    headers = {
        'X-RateLimit-Limit': str(limit),
        'X-RateLimit-Remaining': str(max(0, remaining)),
        'X-RateLimit-Reset': str(int(reset_timestamp)),
    }
    if retry_after is not None:
        headers['Retry-After'] = str(retry_after)
    return headers

import time

# Simulated response for allowed request
headers = build_rate_limit_headers(
    limit=100,
    remaining=73,
    reset_timestamp=time.time() + 45
)
print('Allowed request headers:')
for k, v in headers.items():
    print(f'  {k}: {v}')

# Rate limited response
headers_429 = build_rate_limit_headers(
    limit=100,
    remaining=0,
    reset_timestamp=time.time() + 30,
    retry_after=30
)
print('\n429 Too Many Requests headers:')
for k, v in headers_429.items():
    print(f'  {k}: {v}')

Comparación de algoritmos de limitación de tasa

Comparativa resumida de todos los algoritmos de limitación de tasa para ayudarle a elegir en entrevistas:

  • Token Bucket: permite ráfagas y ofrece una tasa de recarga uniforme. Es la mejor opción para APIs en las que se aceptan ráfagas ocasionales (la opción más habitual).
  • Leaky Bucket: procesa las solicitudes a una tasa de salida fija, independientemente de la ráfaga. Es la mejor opción para regular el tráfico y convertirlo en un flujo constante.
  • Fixed Window Counter: es el más sencillo y utiliza espacio O(1). Problema: permite una ráfaga de hasta el doble del límite en el borde de la ventana (por ejemplo, 100 a las 11:59 + 100 a las 12:00).
  • Sliding Window Log: es el más preciso y no produce picos en el borde de la ventana. Problema: utiliza memoria O(requests).
  • Sliding Window Counter: aproxima el comportamiento de Sliding Window Log utilizando espacio O(1). Cloudflare lo utiliza.
# Algorithm comparison matrix
comparison = [
    ('Token Bucket',          'Allows bursts',     'O(1)',         'Most APIs, default choice'),
    ('Leaky Bucket',          'Smooth output rate', 'O(1)',         'Traffic shaping, message queues'),
    ('Fixed Window Counter',  'Very simple',        'O(1)',         'Low-traffic, approximate OK'),
    ('Sliding Window Log',    'Most accurate',      'O(requests)', 'High-accuracy, low traffic'),
    ('Sliding Window Counter','Approximate+fast',   'O(1)',         'High-traffic, Cloudflare-style'),
]
print(f'{'Algorithm':30s} {'Burst Handling':20s} {'Memory':15s} {'Use Case'}')
print('-'*85)
for name, burst, mem, use in comparison:
    print(f'{name:30s} {burst:20s} {mem:15s} {use}')

Comprobación rápida

Compruebe su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.

Resumen de la lección

En esta lección ha aprendido que los limitadores de tasa utilizan Token Bucket (permite ráfagas), Sliding Window Counter (memoria O(1)) o Sliding Window Log (el más preciso) para controlar las tasas de solicitudes, y que las operaciones atómicas de Redis permiten aplicar limitación de tasa distribuida, y que el feed de Twitter utiliza fan-out on write para precalcular en Redis los feeds de los seguidores y conseguir lecturas rápidas, con un modelo híbrido pull para las cuentas de gran popularidad, lo que evita una amplificación masiva de las escrituras. A continuación, entraremos en la sección final con una guía rápida de reconocimiento de patrones que relaciona las señales de los problemas con los patrones algorítmicos que los resuelven más rápidamente.

Preguntas frecuentes

¿La lección «Diseñar un limitador de solicitudes y un feed de Twitter» es gratis?

Sí — el texto completo de «Diseñar un limitador de solicitudes y un feed de Twitter» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA Interview Prep incluye 4 lecciones en total.

¿Qué aprenderé en «Diseñar un limitador de solicitudes y un feed de Twitter»?

Aplique el marco a dos problemas de diseño canónicos: limitación de solicitudes con token bucket o ventana deslizante, y un feed de noticias con fan-out-on-write frente a fan-out-on-read. Practicas DSA Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar DSA Interview Prep?

No se requiere experiencia previa. DSA Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Diseñar un limitador de solicitudes y un feed de Twitter»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de DSA Interview Prep?

Sí. Cada lección de DSA Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Marco de la entrevista de diseño de sistemas
  2. Almacenamiento de datos escalable: SQL frente a NoSQL
  3. Caché, CDN y balanceo de carga
  4. Diseñar un limitador de solicitudes y un feed de Twitter
← Volver a DSA Interview Prep