Capítulo 12: Rate Limiting com Token Bucket e Proteção de API
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Rate Limiting com Token Bucket e Proteção de API"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Rate Limiting com Token Bucket e Proteção de API
Rate limiting existe para proteger a API contra dois cenários muito diferentes: o cliente legítimo que erra (um loop de retry sem backoff) e o cliente malicioso (scraping agressivo, força bruta em login, DDoS de aplicação). O algoritmo mais usado em produção Go é o Token Bucket, implementado pela biblioteca oficial golang.org/x/time/rate: um “balde” acumula tokens a uma taxa constante (r tokens/segundo) até um limite (burst); cada requisição consome um token, e se o balde estiver vazio a requisição é rejeitada com 429 Too Many Requests. A vantagem sobre um contador fixo por janela é que o Token Bucket absorve picos legítimos (o burst) sem abrir mão do controle da taxa média sustentada — um cliente que enviou 3 requisições rápidas e depois ficou ocioso não é penalizado como se estivesse martelando o servidor.
limiter := rate.NewLimiter(5, 10) // 5 req/s sustentado, burst de até 10
if !limiter.Allow() {
// rejeitar com 429
}
Em Gin, o padrão idiomático é manter um *rate.Limiter por chave de identificação do cliente (tipicamente c.ClientIP(), ou o ID do usuário autenticado quando disponível), guardado em um map[string]*rate.Limiter protegido por sync.Mutex — um único limiter global penalizaria todos os clientes pelo comportamento de um só. Esse mapa em memória tem uma limitação importante: não é compartilhado entre instâncias da API. Em um deployment com múltiplas réplicas atrás de um load balancer, cada instância aplica seu próprio limite independente, então o limite efetivo percebido pelo cliente é limite × número de réplicas. A solução é externalizar o estado para o Redis, usando INCR sobre uma chave por IP/janela de tempo seguido de EXPIRE para expirar a janela — isso implementa uma sliding/fixed window coordenada entre todas as instâncias, ao custo de uma chamada de rede por requisição (mitigável com pipelining ou scripts Lua atômicos).
Rate limiting é só uma camada de uma postura de segurança defensiva mais ampla:
- CORS (
cors.New()do pacotegithub.com/gin-contrib/cors): controla, do lado do navegador, quais origens podem chamar a API via JavaScript. Não é proteção contra chamadas server-to-server oucurl— é uma política que os navegadores respeitam, não o servidor. - Headers de segurança HTTP:
Strict-Transport-Security(força HTTPS em conexões futuras),X-Content-Type-Options: nosniff(impede que o navegador reinterprete oContent-Type),X-Frame-Options: DENY(bloqueia clickjacking via<iframe>) eContent-Security-Policy(restringe de onde scripts/estilos podem ser carregados). - Timeouts no
http.Server: o padrãohttp.ListenAndServe()sem configuração explícita não temReadTimeout,WriteTimeoutnemIdleTimeout— um cliente que abre a conexão e envia bytes lentamente (ataque Slowloris) mantém a goroutine do handler presa indefinidamente, esgotando o pool de conexões. Configurar esses timeouts explicitamente nohttp.Serveré obrigatório em produção. - Blacklisting dinâmico: contabilizar erros 4xx (especialmente 401/403) por IP e bloquear temporariamente quem ultrapassa um limiar é uma defesa simples e eficaz contra brute-force e scanners automatizados, geralmente combinada com uma janela de expiração (ex.: 15 minutos) para não punir permanentemente por um erro isolado.
É importante escolher o código de resposta certo: 429 para rate limit excedido (idealmente acompanhado do header Retry-After), 403 para bloqueio de política/blacklist, e nunca 500 — esses são recusas deliberadas do servidor, não falhas internas.
💻 2. Código de Demonstração Corporativo
package middlewares
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
// LimiterPorIP mantém um Token Bucket independente por endereço IP, evitando
// que o tráfego de um cliente consuma a cota de todos os demais.
type LimiterPorIP struct {
mu sync.Mutex
limiters map[string]*rate.Limiter
r rate.Limit
burst int
}
func NovoLimiterPorIP(reqsPorSegundo float64, burst int) *LimiterPorIP {
return &LimiterPorIP{
limiters: make(map[string]*rate.Limiter),
r: rate.Limit(reqsPorSegundo),
burst: burst,
}
}
func (l *LimiterPorIP) obter(ip string) *rate.Limiter {
l.mu.Lock()
defer l.mu.Unlock()
lim, existe := l.limiters[ip]
if !existe {
lim = rate.NewLimiter(l.r, l.burst)
l.limiters[ip] = lim
}
return lim
}
func (l *LimiterPorIP) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !l.obter(c.ClientIP()).Allow() {
c.Header("Retry-After", "1")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"erro": "limite de requisições excedido"})
return
}
c.Next()
}
}