Rate limiting em Go é uma daquelas decisões que parecem detalhe até a primeira madrugada ruim. Uma API pública recebe tráfego inesperado, um cliente entra em loop, um bot força endpoints caros, uma integração parceira ignora retry com backoff ou uma página popular dispara milhares de requisições em poucos minutos. Sem limite claro, o problema deixa de ser apenas “usuário abusando” e vira indisponibilidade para todo mundo.
Go é muito usado em APIs, gateways, workers e serviços de alta concorrência justamente porque escala bem com goroutines e baixo overhead. Mas a linguagem não elimina a necessidade de controlar consumo. Se cada requisição pode abrir conexão com banco, chamar outro serviço, ler cache, gravar log, publicar evento ou processar JSON grande, você precisa decidir quem pode fazer quantas chamadas por janela de tempo.
Este guia mostra como pensar em rate limiting em APIs Go, quando usar limite em memória, quando levar o controle para Redis, como escrever um middleware HTTP simples e quais sinais observar em produção. Se a sua API ainda está nas fundações, leia também o guia de API REST em Go, o tutorial de Go com Redis cache e o material de observabilidade em Go.
O que rate limiting resolve
Rate limiting define uma política de uso: quantas requisições uma chave pode fazer em determinado intervalo. A chave pode ser IP, usuário autenticado, token de API, conta, rota, tenant, organização ou combinação desses campos. O objetivo é impedir que uma origem consuma capacidade desproporcional.
Casos comuns:
- Proteger login, recuperação de senha e endpoints de autenticação.
- Evitar scraping agressivo em páginas ou APIs públicas.
- Conter clientes com bug de retry infinito.
- Preservar banco de dados e serviços externos caros.
- Aplicar planos comerciais: gratuito, profissional e enterprise.
- Isolar tenants para que uma conta grande não derrube contas pequenas.
Rate limiting não substitui autenticação, autorização, WAF, cache, filas ou boas queries SQL. Ele é uma camada de defesa e controle de capacidade. Em produção, costuma trabalhar junto com timeout, circuit breaker, cache, métricas e limites no proxy ou load balancer.
Escolha a chave certa
O erro mais comum é limitar apenas por IP. Isso funciona para uma API pública simples, mas pode ser injusto quando muitos usuários legítimos estão atrás do mesmo NAT corporativo, VPN, operadora móvel ou proxy. Por outro lado, limitar apenas por usuário autenticado não protege endpoints antes do login.
Uma regra prática:
- Endpoints públicos: comece por IP e rota.
- Endpoints autenticados: prefira
user_id,account_idouapi_key. - Endpoints caros: combine identidade com rota, por exemplo
account_id:/relatorios/exportar. - Produtos B2B: pense em limite por tenant, não só por usuário individual.
Também vale separar limites por criticidade. Um endpoint de GET /health ou GET /status não tem o mesmo custo de um relatório com agregação pesada. Uma rota de login precisa de limite mais restritivo que uma rota de leitura cacheada.
Token bucket: o modelo mental mais útil
O algoritmo token bucket é fácil de entender: cada chave tem um balde com capacidade máxima. Tokens entram no balde em uma taxa constante. Cada requisição consome um token. Se ainda há token, a requisição passa. Se o balde está vazio, a API responde 429 Too Many Requests.
Esse modelo permite rajadas pequenas sem liberar abuso contínuo. Por exemplo, uma conta pode ter capacidade de 20 tokens e reposição de 10 tokens por segundo. Ela consegue fazer 20 chamadas de uma vez, mas, depois disso, precisa respeitar a reposição.
Em Go, a biblioteca golang.org/x/time/rate implementa esse padrão de forma madura para limites em memória:
go get golang.org/x/time/rate
Um limiter por processo é simples e rápido. Ele funciona bem para serviços pequenos, ferramentas internas, CLIs, workers ou APIs com apenas uma instância. Em múltiplas réplicas, porém, cada processo terá seu próprio contador. Se você roda 5 pods e configura 100 requisições por minuto em memória, o limite real pode virar 500 por minuto.
Middleware HTTP simples em Go
Um middleware mínimo usando net/http pode começar assim:
package main
import (
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
type ClientLimiter struct {
limiter *rate.Limiter
lastSeen time.Time
}
type RateLimitMiddleware struct {
mu sync.Mutex
clients map[string]*ClientLimiter
rate rate.Limit
burst int
}
func NewRateLimitMiddleware(r rate.Limit, burst int) *RateLimitMiddleware {
return &RateLimitMiddleware{
clients: make(map[string]*ClientLimiter),
rate: r,
burst: burst,
}
}
func (m *RateLimitMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := clientKey(r)
limiter := m.getLimiter(key)
if !limiter.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func (m *RateLimitMiddleware) getLimiter(key string) *rate.Limiter {
m.mu.Lock()
defer m.mu.Unlock()
client, ok := m.clients[key]
if !ok {
limiter := rate.NewLimiter(m.rate, m.burst)
m.clients[key] = &ClientLimiter{limiter: limiter, lastSeen: time.Now()}
return limiter
}
client.lastSeen = time.Now()
return client.limiter
}
func clientKey(r *http.Request) string {
if userID := r.Header.Get("X-User-ID"); userID != "" {
return "user:" + userID
}
return "ip:" + r.RemoteAddr
}
Esse exemplo é didático, não final. Em uma API real, X-User-ID deveria vir de autenticação validada, não de header aceito diretamente do cliente. Para IP, você também precisa tratar proxy reverso com cuidado: usar X-Forwarded-For sem confiar apenas no load balancer abre brecha para spoofing.
Limpeza de memória importa
Um mapa de limiters por IP ou usuário pode crescer para sempre. Em tráfego público, basta um atacante variar IPs ou chaves para consumir memória aos poucos. Por isso, mantenha lastSeen e rode uma limpeza periódica:
func (m *RateLimitMiddleware) Cleanup(interval, maxIdle time.Duration, stop <-chan struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
cutoff := time.Now().Add(-maxIdle)
m.mu.Lock()
for key, client := range m.clients {
if client.lastSeen.Before(cutoff) {
delete(m.clients, key)
}
}
m.mu.Unlock()
case <-stop:
return
}
}
}
Esse cuidado parece pequeno, mas é o tipo de detalhe que diferencia um middleware de tutorial de um componente que pode ficar meses no ar.
Quando usar Redis
Use Redis quando o limite precisa ser compartilhado entre várias instâncias da API. Isso é comum em Kubernetes, Cloud Run, ECS, Fly.io, Render, Railway, servidores com autoscaling ou qualquer ambiente em que múltiplos processos atendem a mesma rota.
O desenho comum usa contador com TTL:
INCR rate:user:123:/api/pedidos:2026-05-22T03:10
EXPIRE rate:user:123:/api/pedidos:2026-05-22T03:10 60
Se o contador passar do limite, a API responde 429. Para evitar corrida entre INCR e EXPIRE, muitos times usam script Lua no Redis, porque a execução é atômica. Outra alternativa é usar uma biblioteca que já implemente token bucket ou sliding window com Redis de forma testada.
Redis adiciona custo operacional e latência, então não use por reflexo. Se a API tem uma única instância, limite em memória pode ser suficiente. Se a API escala horizontalmente e o limite tem impacto comercial ou de segurança, Redis costuma ser a opção pragmática.
Headers úteis para clientes
Uma API bem comportada informa o limite ao cliente. Além do status 429, considere retornar headers como:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1779424200
Retry-After: 30
O Retry-After é especialmente importante para integrações. Ele reduz retries cegos e ajuda clientes a implementarem backoff. Em APIs públicas, documente a política de limite na página de desenvolvedores. Em APIs internas, registre o contrato em ADR, README ou documentação do serviço.
Observabilidade: limite sem métrica é chute
Rate limiting precisa aparecer nos dashboards. No mínimo, monitore:
- Total de requisições bloqueadas por rota.
- Percentual de
429por cliente, tenant ou API key. - Latência do backend de limite, se usar Redis.
- Número de chaves ativas em memória.
- Rotas mais atingidas por limite.
- Relação entre
429, CPU, memória, conexões de banco e erros 5xx.
Com slog em Go, registre a chave de limite de forma segura, sem expor tokens completos. Em métricas Prometheus, cuidado com cardinalidade: não crie uma label com user_id para milhões de usuários. Agregue por rota, plano, tipo de cliente ou resultado.
Para tracing com OpenTelemetry, um atributo como rate_limited=true em spans de requisição ajuda a explicar quedas de tráfego aparente. Mas não transforme cada chave de cliente em atributo de alta cardinalidade.
Testes que valem a pena
Teste rate limiting com relógio controlado quando possível. Testes que dependem de time.Sleep longo ficam lentos e instáveis. Para middleware simples, já vale cobrir:
- Requisições abaixo do limite retornam 200.
- Requisições acima do limite retornam 429.
- Chaves diferentes não competem entre si.
- A limpeza remove clientes inativos.
- Headers esperados aparecem na resposta bloqueada.
- Usuário autenticado tem prioridade sobre IP quando aplicável.
Se você usa Redis, rode testes de integração com container local ou ambiente efêmero no CI. O comportamento atômico do script Lua, TTL e concorrência precisa ser validado fora de mocks. O guia de testes de tabela em Go ajuda a organizar esses cenários sem duplicar código de teste.
Erros comuns em produção
Alguns problemas aparecem repetidamente em APIs Go:
- Limite por IP atrás de proxy mal configurado, tratando todo tráfego como um único cliente.
- Aceitar
X-Forwarded-Fordireto da internet. - Não limitar endpoints de login e recuperação de senha.
- Aplicar o mesmo limite para rota barata e rota cara.
- Retornar apenas 500 quando Redis falha, derrubando a API inteira.
- Não limpar limiters antigos em memória.
- Medir
429como erro genérico e gerar alerta falso. - Criar labels Prometheus com cardinalidade explosiva.
Decida também o modo de falha. Se Redis ficar indisponível, a API deve abrir, fechar ou aplicar um limite local temporário? Não existe resposta universal. Para login e segurança, talvez seja melhor falhar fechado. Para leitura pública de baixo risco, falhar aberto com alerta pode preservar disponibilidade.
Rate limiting e carreira backend
Rate limiting aparece em entrevistas porque conecta código, arquitetura e operação. Uma pessoa desenvolvedora Go sênior deve conseguir explicar não só o middleware, mas também o impacto de múltiplas réplicas, Redis, proxy reverso, métricas, segurança e experiência do cliente.
Esse conhecimento também conversa com outras linguagens de sistemas. Se você compara Go com Rust para backend de baixa latência, o portal Rust Brasil tem um guia de concorrência e async em Rust que ajuda a contrastar modelos de runtime, tarefas assíncronas e controle de capacidade.
Para quem busca vagas, rate limiting é um bom assunto para portfólio: implemente uma API pequena com limite por API key, Redis, métricas e testes. Depois, explique as decisões no README. Muitas vagas Go citam microserviços, Kubernetes, Redis, observabilidade, segurança e Clean Architecture em Go; um projeto assim demonstra maturidade além de CRUD.
Checklist prático
Antes de colocar rate limiting em produção, revise:
- Qual é a chave de limite: IP, usuário, API key, tenant ou rota?
- O serviço roda em uma ou várias instâncias?
- O limite precisa ser global, por rota ou por plano comercial?
- O cliente recebe
429comRetry-After? - Redis é obrigatório ou limite local basta?
- A falha do backend de limite abre ou fecha o tráfego?
- Existem métricas para bloqueios, latência e cardinalidade?
- Endpoints sensíveis têm limites próprios?
- O proxy reverso está configurado para preservar IP real com segurança?
- Os testes cobrem concorrência e estouro de limite?
Conclusão
Rate limiting em Go não deve ser tratado como enfeite de middleware. Ele é parte da estratégia de confiabilidade da API. Comece com uma política simples, escolha uma chave justa, use golang.org/x/time/rate quando o limite local bastar e leve o controle para Redis quando houver múltiplas réplicas ou contrato comercial envolvido.
O ponto mais importante é tornar o limite explícito e observável. Uma API Go em produção precisa continuar rápida para usuários legítimos mesmo quando clientes erram, bots insistem ou o tráfego cresce de repente. Rate limiting bem desenhado transforma esse cenário de incidente em comportamento esperado.