Progettare un rate limiter e un feed di Twitter
Applichi il framework a due problemi canonici di design: il rate limiting con token bucket o sliding window e un news feed con fan-out-on-write o fan-out-on-read.
Progettare un rate limiter e un feed di Twitter è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.
Perché il rate limiting è essenziale
Il rate limiting controlla il numero di richieste che un client può inviare a un'API in una determinata finestra temporale. Senza questo controllo, un singolo client che si comporta in modo errato (o un attacco DDoS) può saturare le risorse del server, degradando il servizio per tutti gli utenti. Il rate limiting protegge anche dagli attacchi brute-force, impedisce lo scraping delle API e impone un uso equo delle risorse condivise.
Granularità comuni del rate limiting: per ID utente, per chiave API, per indirizzo IP, per endpoint o una combinazione di questi criteri. Limiti tipici: 100 richieste al minuto per utente, 1000 all'ora per chiave API. Il rate limiter deve essere veloce (aggiungendo un overhead di <1ms) e distribuito (coerente tra tutte le repliche del server 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 di rate limiting 1: token bucket
L'algoritmo token bucket mantiene un bucket con una capacità massima di N token. I token vengono aggiunti a una frequenza fissa (ad es. 10 al secondo). Ogni richiesta consuma un token. Se il bucket è vuoto, la richiesta viene rifiutata. Se non ha raggiunto la capacità massima, la richiesta viene accettata e il token viene consumato.
Il token bucket consente i burst: se non arrivano richieste per 5 secondi, il bucket si riempie fino a N token e quindi N richieste possono arrivare immediatamente. È adatto alle API in cui sono accettabili raffiche occasionali. I due parametri sono la capacità (dimensione del burst) e la frequenza di rifornimento.
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 requestsAlgoritmo di rate limiting 2: sliding window log
Lo sliding window log memorizza un timestamp per ogni richiesta in un insieme ordinato. Per ogni nuova richiesta, rimuove i timestamp più vecchi dell'inizio della finestra, quindi verifica se il numero di timestamp rimanenti è inferiore al limite. In caso affermativo, aggiunge il timestamp corrente e autorizza la richiesta; altrimenti la rifiuta.
Questo approccio è preciso: conta esattamente quante richieste sono state effettuate negli ultimi N secondi. Il compromesso è l'elevato consumo di memoria (una voce per richiesta per utente). Con un limite di 1000 richieste al minuto per 100K utenti, nel caso peggiore si arriva a 100M di voci di log. Non è adatto a traffico molto elevato, a meno che non venga combinato con lo 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 di rate limiting 3: sliding window counter
Lo sliding window counter approssima la finestra scorrevole utilizzando due bucket — il minuto corrente e quello precedente — pesati in base a quanto è trascorso del minuto corrente. Questo riduce la memoria da O(requests) a O(1) per utente, approssimando con grande precisione il conteggio esatto della finestra scorrevole.
Formula: estimated_count = prev_count × (1 - fraction_of_window_elapsed) + curr_count. Se questo conteggio stimato supera il limite, la richiesta viene rifiutata. Questo è l'algoritmo utilizzato da Cloudflare e Kong su larga scala, grazie alla memoria O(1) per utente e all'elevata precisione.
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)Rate limiting distribuito con Redis
In un sistema distribuito con più server applicativi, il rate limiting deve essere centralizzato; in caso contrario ogni server tiene traccia del proprio conteggio e i limiti vengono di fatto moltiplicati per il numero di server. Redis con operazioni atomiche è la soluzione standard: si utilizzano INCR ed EXPIRE per un contatore a finestra fissa, oppure ZADD e ZCOUNT per un log a finestra scorrevole.
L'approccio basato su script Lua rende atomiche più operazioni Redis, evitando condizioni di competizione in cui due server incrementano contemporaneamente quando il conteggio è appena al di sotto del limite. Redis esegue gli script Lua come un unico comando, garantendo l'atomicità senza lock distribuiti.
# 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')Progettazione del feed di Twitter: requisiti
Progettiamo un sistema di feed di notizie simile a Twitter. Requisiti funzionali: gli utenti possono pubblicare tweet (fino a 280 caratteri), seguire altri utenti e visualizzare un feed dei tweet delle persone che seguono, ordinati per data. Requisiti non funzionali: 300M di utenti attivi giornalieri, 500M di tweet al giorno, il feed deve caricarsi in <2 secondi, rapporto letture:scritture ~100:1.
Stime della capacità: 500M di tweet al giorno ÷ 86400 ≈ 5800 tweet al secondo. Le letture sono circa 580K al secondo. Ogni tweet occupa circa 300 byte; 500M × 300B = 150 GB al giorno di nuovo spazio di archiviazione per i tweet. L'aggregazione del feed è la principale sfida ingegneristica.
# 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 in scrittura: feed precalcolati
Nel fan-out on write, quando l'utente A pubblica un tweet, il sistema lo distribuisce immediatamente nel feed di ogni follower. Quando un follower richiede il proprio feed, questo è già precalcolato e memorizzato in Redis: basta leggere una lista Redis con complessità O(k), dove k è la dimensione del feed (in genere limitata a 1000 tweet).
La difficoltà riguarda le celebrità con milioni di follower, che generano enormi operazioni di fan-out. La pubblicazione di un tweet da parte di Justin Bieber richiede di scrivere contemporaneamente nei feed di oltre 100M di follower: un problema reale affrontato da Twitter e chiamato 'celebrity problem'. Il servizio di fan-out in scrittura deve essere asincrono e basato su code per gestire questi picchi.
# 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 ibrido: risolvere il celebrity problem
L'approccio ibrido combina il fan-out on write per gli utenti normali e il fan-out on read per le celebrità. Un utente viene classificato come celebrità se il suo numero di follower supera una soglia (ad es. 1 milione di follower). Per gli utenti normali, i tweet vengono inviati a tutti i feed dei follower al momento della pubblicazione. Per le celebrità, i tweet NON vengono inviati; invece, quando un follower legge il proprio feed, il sistema recupera i tweet recenti della celebrità e li unisce al feed precalcolato.
Questo modello ibrido è simile a quello effettivamente utilizzato da Twitter. Il passaggio di merge è rapido perché le celebrità pubblicano raramente e il merge è O(f), dove f è il numero di account di celebrità seguiti dall'utente (in genere ridotto).
# 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 di Twitter: architettura completa
L'architettura completa del feed di Twitter combina diversi sistemi:
- Servizio dei tweet: scrive i tweet in Cassandra (elevato throughput di scrittura, serie temporali)
- Servizio di fan-out: worker asincroni (consumer Kafka) che inviano gli ID dei tweet ai feed dei follower in Redis
- Servizio del feed: legge dal feed Redis, recupera gli oggetti completi dei tweet a partire dagli ID, unisce i tweet delle celebrità
- Servizio dei follow: gestisce il grafo sociale (chi segue chi) in un graph DB o in SQL con sharding
- Servizio della timeline: fornisce i tweet pubblicati dall'utente (separatamente dal feed principale)
# 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)Intestazioni e risposte di errore del rate limiter
Un rate limiter ben progettato comunica i propri limiti ai client tramite le intestazioni delle risposte HTTP. In questo modo i client possono implementare la logica di retry-after e i dashboard possono visualizzare l'utilizzo. Intestazioni standard:
X-RateLimit-Limit: numero massimo di richieste consentite nella finestraX-RateLimit-Remaining: richieste rimanenti nella finestra correnteX-RateLimit-Reset: timestamp Unix del momento in cui la finestra viene reimpostataRetry-After: secondi da attendere prima di ritentare (in caso di risposta 429)
Il codice di stato HTTP per le risposte limitate dal rate limiting è 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}')Confronto tra gli algoritmi di rate limiting
Confronto riepilogativo di tutti gli algoritmi di rate limiting per aiutarLa a scegliere durante i colloqui:
- Token Bucket: consente i burst e offre un tasso di ricarica uniforme. È la scelta migliore per le API in cui sono accettabili burst occasionali (e la scelta più comune).
- Leaky Bucket: elabora le richieste a un tasso di uscita fisso, indipendentemente dai burst. È la scelta migliore per modellare il traffico come un flusso costante.
- Fixed Window Counter: è il più semplice e usa spazio O(1). Problema: un burst pari al doppio del limite al confine della finestra (ad esempio, 100 alle 11:59 + 100 alle 12:00).
- Sliding Window Log: è il più accurato e non presenta picchi ai confini. Problema: usa memoria O(richieste).
- Sliding Window Counter: approssima lo sliding log usando spazio O(1). Viene utilizzato da Cloudflare.
# 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}')Verifica rapida
Verifichi la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep presentati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: i rate limiter utilizzano token bucket (consente i burst), sliding window counter (memoria O(1)) oppure sliding window log (il più accurato) per controllare il tasso delle richieste, mentre le operazioni atomiche di Redis consentono il rate limiting distribuito; inoltre, il feed di Twitter utilizza il fan-out on write per precalcolare in Redis i feed dei follower e ottenere letture rapide, con un modello ibrido pull per gli account di celebrità, così da evitare un'enorme write amplification. Ora passerà alla sezione capstone, con un cheat sheet per il riconoscimento dei pattern che collega i segnali dei problemi ai pattern algoritmici in grado di risolverli più rapidamente.
Impara Coding Interview Prep con un tutor IA — gratis
Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.
- Corsi
- 90
- Lezioni
- 360
Domande Frequenti
La lezione «Progettare un rate limiter e un feed di Twitter» è gratuita?
Sì — il testo completo di «Progettare un rate limiter e un feed di Twitter» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Progettare un rate limiter e un feed di Twitter»?
Applichi il framework a due problemi canonici di design: il rate limiting con token bucket o sliding window e un news feed con fan-out-on-write o fan-out-on-read. Eserciti Coding Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Coding Interview Prep?
Non è richiesta alcuna esperienza precedente. Coding Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.
Quanto tempo richiede la lezione «Progettare un rate limiter e un feed di Twitter»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Coding Interview Prep?
Sì. Ogni lezione Coding Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Il framework per i colloqui di system design
- Archiviazione scalabile dei dati: SQL e NoSQL
- Caching, CDN e bilanciamento del carico
- Progettare un rate limiter e un feed di Twitter