Projetar um limitador de taxa e um feed do Twitter
Aplique a estrutura a dois problemas clássicos de projeto: limitação de taxa por depósito de tokens ou janela deslizante e feed de notícias com distribuição na escrita ou na leitura.
Projetar um limitador de taxa e um feed do Twitter é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding Interview Prep inclui 4 aulas no total.
Por que a limitação de taxa é essencial
A limitação de taxa controla o número de solicitações que um cliente pode fazer a uma API em uma determinada janela de tempo. Sem ela, um único cliente com comportamento inadequado (ou um ataque DDoS) pode saturar os recursos do servidor, degradando o serviço para todos os usuários. A limitação de taxa também protege contra ataques de força bruta, impede a extração automatizada de dados da API e impõe o uso justo de recursos compartilhados.
Granularidades comuns da limitação de taxa: por ID de usuário, por chave de API, por endereço IP, por endpoint ou uma combinação desses critérios. Limites típicos: 100 solicitações por minuto por usuário, 1000 por hora por chave de API. O limitador de taxa precisa ser rápido (adicionando menos de 1 ms de sobrecarga) e distribuído (consistente em todas as réplicas do 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 1 de limitação de taxa: balde de tokens
O algoritmo de balde de tokens mantém um balde com capacidade máxima de N tokens. Os tokens são adicionados a uma taxa fixa (por exemplo, 10 por segundo). Cada solicitação consome um token. Se o balde estiver vazio, a solicitação será rejeitada. Se estiver abaixo da capacidade, ela será aceita e o token será consumido.
O balde de tokens permite rajadas: se nenhuma solicitação chegar durante 5 segundos, o balde se encherá até N tokens e, em seguida, N solicitações poderão chegar imediatamente. Isso é apropriado para APIs em que rajadas ocasionais são aceitáveis. Os dois parâmetros são a capacidade (tamanho da rajada) e a taxa de reposição.
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 2 de limitação de taxa: registro de janela deslizante
O registro de janela deslizante armazena um carimbo de data e hora para cada solicitação em um conjunto ordenado. Para cada nova solicitação, remova os carimbos de data e hora anteriores ao início da janela e verifique se a quantidade de carimbos restantes está abaixo do limite. Se estiver, adicione o carimbo de data e hora atual e permita a solicitação; caso contrário, rejeite-a.
Esse método é preciso — conta exatamente quantas solicitações ocorreram nos últimos N segundos. A desvantagem é o alto uso de memória (uma entrada por solicitação por usuário). Para um limite de 1000 solicitações por minuto com 100 mil usuários, o pior caso é de 100 milhões de entradas de registro. Não é adequado para tráfego muito intenso, a menos que seja combinado com fragmentação.
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 3 de limitação de taxa: contador de janela deslizante
O contador de janela deslizante aproxima a janela deslizante usando dois baldes — o minuto atual e o minuto anterior — ponderados de acordo com o quanto já avançamos no minuto atual. Isso reduz o uso de memória de O(solicitações) para O(1) por usuário, ao mesmo tempo que aproxima de perto a contagem exata da janela deslizante.
Fórmula: estimated_count = prev_count × (1 - fraction_of_window_elapsed) + curr_count. Se essa contagem estimada exceder o limite, rejeite a solicitação. Esse é o algoritmo usado pelo Cloudflare e pelo Kong em grande escala, devido ao uso de memória O(1) por usuário e à alta precisão.
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)Limitação de taxa distribuída com Redis
Em um sistema distribuído com vários servidores de aplicação, a limitação de taxa precisa ser centralizada — caso contrário, cada servidor mantém sua própria contagem e os limites são efetivamente multiplicados pela quantidade de servidores. O Redis, com operações atômicas, é a solução padrão: use INCR e EXPIRE para um contador de janela fixa ou ZADD e ZCOUNT para um registro de janela deslizante.
A abordagem com script Lua torna atômicas várias operações do Redis, evitando condições de corrida nas quais dois servidores incrementam simultaneamente quando estão pouco abaixo do limite. O Redis processa scripts Lua como um único comando, garantindo atomicidade sem bloqueios distribuídos.
# 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')Projetando o fluxo do Twitter: requisitos
Vamos projetar um sistema de fluxo de notícias semelhante ao Twitter. Requisitos funcionais: os usuários podem publicar tuítes (com até 280 caracteres), seguir outros usuários e visualizar um fluxo de tuítes das pessoas que seguem, ordenado por recência. Requisitos não funcionais: 300 milhões de usuários ativos diariamente, 500 milhões de tuítes por dia, o fluxo deve carregar em menos de 2 segundos, proporção leitura:escrita de aproximadamente 100:1.
Estimativas de capacidade: 500 milhões de tuítes/dia ÷ 86400 ≈ 5800 tuítes/segundo. Leituras ≈ 580 mil/segundo. Cada tuíte ≈ 300 bytes; 500 milhões × 300 B = 150 GB/dia de novo armazenamento de tuítes. A agregação do fluxo é o principal desafio de engenharia.
# 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()Propagação na escrita: fluxos pré-computados
Na propagação na escrita, quando o usuário A publica um tuíte, o sistema o distribui imediatamente para o fluxo de cada seguidor. Quando um seguidor solicita seu fluxo, ele já está pré-computado e armazenado no Redis — uma simples leitura de lista do Redis com O(k), em que k é o tamanho do fluxo (normalmente limitado a 1000 tuítes).
O desafio são as celebridades com milhões de seguidores, que geram operações de propagação em massa. Quando Justin Bieber publica um tuíte, é necessário escrevê-lo simultaneamente nos fluxos de mais de 100 milhões de seguidores — um problema real enfrentado pelo Twitter e chamado de 'problema das celebridades'. O serviço de propagação de escrita precisa ser assíncrono e baseado em filas para lidar com esses 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')Propagação híbrida: resolvendo o problema das celebridades
A abordagem híbrida combina a propagação na escrita para usuários comuns com a propagação na leitura para celebridades. Um usuário é classificado como celebridade quando sua quantidade de seguidores ultrapassa um limite (por exemplo, 1 milhão de seguidores). Para usuários comuns, os tuítes são enviados para todos os fluxos dos seguidores no momento da publicação. Para celebridades, seus tuítes NOT são enviados; em vez disso, quando um seguidor lê seu fluxo, o sistema busca os tuítes recentes da celebridade e os mescla ao fluxo pré-computado.
Esse modelo híbrido é próximo do que o Twitter realmente usa. A etapa de mesclagem é rápida porque as celebridades publicam raramente, e a mesclagem é O(f), em que f é o número de contas de celebridades seguidas pelo usuário (normalmente pequeno).
# 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)Fluxo do Twitter: arquitetura completa
A arquitetura completa do fluxo do Twitter combina vários sistemas:
- Serviço de tuítes: grava tuítes no Cassandra (alto volume de escrita, séries temporais)
- Serviço de propagação: processos assíncronos (consumidores do Kafka) que enviam identificadores de tuítes para os fluxos dos seguidores no Redis
- Serviço de fluxo: lê o fluxo no Redis, transforma os identificadores de tuítes em objetos completos e mescla os tuítes de celebridades
- Serviço de seguimento: gerencia o grafo social (quem segue quem) em um banco de dados de grafos ou em SQL fragmentado
- Serviço de linha do tempo: fornece os tuítes do próprio usuário (separado do fluxo inicial)
# 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)Cabeçalhos do limitador de taxa e respostas de erro
Um limitador de taxa bem projetado comunica seus limites aos clientes por meio de cabeçalhos de resposta HTTP. Isso permite que os clientes implementem uma lógica de nova tentativa e que os painéis exibam o uso. Cabeçalhos padrão:
X-RateLimit-Limit: máximo de solicitações permitidas na janelaX-RateLimit-Remaining: solicitações restantes na janela atualX-RateLimit-Reset: carimbo de data e hora Unix em que a janela é redefinidaRetry-After: quantidade de segundos a aguardar antes de tentar novamente (em uma resposta 429)
O código de status HTTP para respostas limitadas por taxa é 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}')Comparação entre algoritmos de limitação de taxa
Comparação resumida de todos os algoritmos de limitação de taxa para ajudar você a escolher em entrevistas:
- Balde de tokens: permite rajadas, com taxa de reposição uniforme. Melhor para APIs em que rajadas ocasionais são aceitáveis (a escolha mais comum).
- Balde com vazamento: processa requisições a uma taxa de saída fixa, independentemente da rajada. Melhor para moldar o tráfego como um fluxo constante.
- Contador de janela fixa: mais simples, com espaço O(1). Problema: uma rajada de duas vezes o limite na borda da janela (por exemplo, 100 às 11:59 + 100 às 12:00).
- Registro de janela deslizante: mais preciso, sem pico nas bordas. Problema: memória O(requisições).
- Contador de janela deslizante: aproxima o registro deslizante usando espaço O(1). Usado pelo 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ção rápida
Teste sua compreensão dos conceitos de Estruturas de Dados & Algoritmos — Preparação para Entrevistas de Programação desta lição.
Recapitulação da lição
Nesta lição, você aprendeu: os limitadores de taxa usam o balde de tokens (permite rajadas), o contador de janela deslizante (memória O(1)) ou o registro de janela deslizante (mais preciso) para controlar as taxas de requisições, e as operações atômicas do Redis permitem a limitação de taxa distribuída; o fluxo do Twitter usa distribuição na escrita para pré-computar os fluxos dos seguidores no Redis e obter leituras rápidas, com um modelo híbrido de busca para contas de celebridades, evitando uma enorme amplificação de escrita. A seguir, entraremos na seção do projeto final com uma folha de consulta rápida de reconhecimento de padrões que mapeia sinais dos problemas para os padrões algorítmicos que os resolvem mais rapidamente.
Perguntas Frequentes
A aula “Projetar um limitador de taxa e um feed do Twitter” é grátis?
Sim — o texto completo de “Projetar um limitador de taxa e um feed do Twitter” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding Interview Prep inclui 4 aulas no total.
O que vou aprender em “Projetar um limitador de taxa e um feed do Twitter”?
Aplique a estrutura a dois problemas clássicos de projeto: limitação de taxa por depósito de tokens ou janela deslizante e feed de notícias com distribuição na escrita ou na leitura. Você pratica Coding Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Coding Interview Prep?
Nenhuma experiência prévia é necessária. Coding Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Projetar um limitador de taxa e um feed do Twitter”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Coding Interview Prep?
Sim. Cada aula de Coding Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- A estrutura da entrevista de projeto de sistemas
- Armazenamento de dados escalável: SQL vs NoSQL
- Cache, CDNs e balanceamento de carga
- Projetar um limitador de taxa e um feed do Twitter