Rate limiting por usuário em Spring Boot com Redis (Token Bucket) — passo a passo
Por John Pires · 19 de março de 2026

Rate limiting por usuário em Spring Boot com Redis (Token Bucket) — passo a passo
Por que aprender isso em 2026
Contexto técnico atual
Em 2026, aplicações distribuídas continuam crescendo: microservices, APIs públicas, UIs ricas e clientes móveis. Serviços expostos atraem tráfego imprevisível — picos legítimos, crawlers mal configurados e ataques simples de negação de serviço. Limitar taxa (rate limiting) não é só proteção; é componente de resiliência para manter latência e custos sob controle.
Benefícios concretos
Um sistema de rate limiting por usuário mantém qualidade de serviço, evita abuso e permite planos diferenciados para clientes. Usar Redis como store centralizado permite aplicar políticas consistentes entre múltiplas instâncias do serviço sem perda de atomicidade, quando combinado com scripts Lua ou operações atômicas.
Analogia simples: pense no token bucket como um balde que recebe gotas (tokens) a uma taxa fixa. Cada requisição pega uma gota. Se o balde esvazia, o cliente precisa esperar.
Dica: começar com um protótipo local e depois testar em ambiente com várias instâncias é essencial para validar a corretude distribuída.
Configuração do ambiente
Dependências mínimas (pom.xml)
Antes do código, configure o projeto. Abaixo está um pom.xml mínimo para Spring Boot com Redis e web. O objetivo é permitir execução local, com Redis em container ou local.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.exemplo</groupId>
<artifactId>rate-limiter-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
O pom.xml acima é funcional: substitua a versão do Spring Boot se necessário. Em ambientes reais, adicione monitoramento, testes e configurações de segurança.
Redis local
Para testar localmente, use Docker:
docker run -p 6379:6379 --name redis-local -d redis:7.0
Isso inicia o Redis na porta padrão. Spring Boot detecta automaticamente se você usa as propriedades padrão (localhost:6379). Em producao, use Redis cluster ou managed Redis, com TLS/ACL conforme necessário.
Hello World progressivo
Abordagem errada: limiter em memória (única instância)
Para iniciantes, é comum implementar um rate limiter em memória. Abaixo um exemplo funcional que ilustra o problema: funciona só em uma instância e perde estado se a aplicação reiniciar. Isso é útil para aprendizado, mas não para produção em ambiente com múltiplas réplicas.
package com.exemplo.ratelimit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class InMemoryRateLimiter {
private final ConcurrentHashMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
private final long windowMs = 1000; // 1 segundo
private final long maxRequests = 5;
public boolean tryConsume(String userId) {
String key = userId + ":" + (System.currentTimeMillis() / windowMs);
AtomicLong counter = counters.computeIfAbsent(key, k -> new AtomicLong(0));
long value = counter.incrementAndGet();
return value <= maxRequests;
}
}
O código acima faz contagem por janelas fixas e é simples. Problemas:
- Não funciona entre instâncias diferentes (não distribuído).
- Edge cases na transição de janelas causam bursts.
- Não é persistente e reinício zera contadores.
Abordagem correta: token bucket no Redis
Token bucket é mais tolerante a rajadas e permite refil incremental. Em ambiente distribuído, qualquer operação que altera tokens precisa ser atômica. Redis + script Lua resolve isso: o script executa a lógica inteira do token bucket no servidor Redis, garantindo atomicidade.
Vamos ver o script Lua e a integração com Spring usando StringRedisTemplate. O script assume medida em milissegundos e tokens como número inteiro.
-- redis_token_bucket.lua
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1]) -- tokens por segundo
local capacity = tonumber(ARGV[2])
local now_millis = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity / rate * 1000
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then last_tokens = capacity end
local last_refill = tonumber(redis.call("get", timestamp_key))
if last_refill == nil then last_refill = now_millis end
local delta = math.max(0, now_millis - last_refill)
local new_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))
if new_tokens < requested then
-- não há tokens suficientes
redis.call("set", tokens_key, new_tokens)
redis.call("set", timestamp_key, now_millis)
return 0
else
new_tokens = new_tokens - requested
redis.call("set", tokens_key, new_tokens)
redis.call("set", timestamp_key, now_millis)
return 1
end
Contexto: o script atualiza o número de tokens com base no tempo desde o último refill e tenta consumir tokens. O retorno é 1 (permitir) ou 0 (bloquear).
Agora o serviço Java que executa esse script via Spring.
package com.exemplo.ratelimit;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.Arrays;
@Service
public class RedisTokenBucketService {
private final StringRedisTemplate redisTemplate;
private final RedisScript<Long> script;
private final int ratePerSecond = 5; // tokens por segundo
private final int capacity = 10;
private static final String LUA = "..."; // coloque o conteúdo do script acima aqui
public RedisTokenBucketService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.script = RedisScript.of(LUA, Long.class);
}
public boolean allowRequest(String userId) {
String tokensKey = "tb:" + userId + ":tokens";
String tsKey = "tb:" + userId + ":ts";
long now = Instant.now().toEpochMilli();
Long result = redisTemplate.execute(script, Arrays.asList(tokensKey, tsKey),
String.valueOf(ratePerSecond), String.valueOf(capacity), String.valueOf(now), "1");
return result != null && result == 1L;
}
}
O que acontece depois da execução: o Redis atualiza e retorna 1/0. A chamada é rápida e segura mesmo com múltiplas réplicas da aplicação.
Mini-projeto prático
Implementação completa: filtro que aplica rate limit por usuário
Agora integramos o serviço ao pipeline HTTP. A estratégia: extrair userId de um header HTTP (X-User-Id) ou IP, então chamar RedisTokenBucketService. Em caso de bloqueio, devolvemos 429 com Retry-After.
package com.exemplo.ratelimit;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RateLimitFilter extends HttpFilter {
private final RedisTokenBucketService tokenBucketService;
public RateLimitFilter(RedisTokenBucketService tokenBucketService) {
this.tokenBucketService = tokenBucketService;
}
@Override
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String userId = req.getHeader("X-User-Id");
if (userId == null || userId.isBlank()) {
userId = req.getRemoteAddr();
}
boolean allowed = tokenBucketService.allowRequest(userId);
if (!allowed) {
res.setStatus(429);
res.setHeader("Retry-After", "1"); // sugestão simples
res.getWriter().write("Too Many Requests\n");
return;
}
chain.doFilter(req, res);
}
}
Contexto: o filtro roda para cada requisição. Antes de delegar ao controller, verifica se o usuário tem tokens. Depois: se permitido, a requisição segue; se não, devolvemos 429.
Testando localmente
Teste com curl simulando um usuário:
for i in {1..15}; do curl -s -o /dev/null -w "%{http_code}\n" -H "X-User-Id: alice" http://localhost:8080/some-endpoint; done
Você verá códigos 200 até os tokens acabarem, depois 429. Esse ciclo demonstra a mecânica do token bucket.
Erros comuns, armadilhas e dicas práticas
Implementações erradas são frequentes. Aqui estão problemas comuns e como corrigi-los:
- Usar armazenamento local: funciona em dev, falha em cluster. Use Redis para ambientes com réplicas.
- Não usar script atômico: operações separadas GET + SET permitem condições de corrida.
- Timeouts de Redis ignorados: configure timeouts e circuit breakers — falha do Redis não deve travar a aplicação inteira.
- Chaves sem expiração: defina TTLs para evitar crescimento indefinido do keyspace.
- Medidas temporais erradas: sincronize unidades (ms vs s) entre Java e Lua.
Prática de code review: sempre verifique atomicidade, unidades de tempo e TTL das chaves quando usar Redis para leveduras de controle.
Recursos para continuar estudando
Links e leituras recomendadas
Para aprofundar, estude os seguintes tópicos e materiais:
- Documentação do Redis sobre scripting Lua e comandos GET/SET/INCR/EXPIRE.
- Artigos sobre token bucket vs leaky bucket e sliding window.
- Livros e cursos sobre arquitetura de microsserviços e patterns de resiliência (circuit breaker, bulkhead).
Próximos passos práticos
Experimente variações e melhorias:
- Adicionar métricas (Prometheus) para monitorar rejeições e latência.
- Suportar planos diferenciados por usuário (rates e capacidades por cliente).
- Implementar fallback: quando Redis estiver indisponível, decidir entre falhar aberto (permitir) ou fechado (bloquear) com base na criticidade.
Também valeria a pena migrar o script para Redis Modules ou usar uma biblioteca pronta se o time preferir convenção sobre configuração.
Conclusão
Resumo prático
Você viu duas abordagens: a errada (in-memory) e a correta para ambiente distribuído (token bucket em Redis com script Lua). A in-memory é fácil para testes locais; a solução com Redis é atômica e escalável. Integrar o serviço ao pipeline HTTP com um filtro assegura que requisições sejam barradas antes de chegar ao processamento pesado.
Checklist de implantação
Antes de mover para produção, valide estes itens:
- Configurar Redis com alta disponibilidade e segurança.
- Testar limites sob carga e com múltiplas instâncias.
- Adicionar TTL nas chaves do token bucket para limpeza automática.
- Instrumentar métricas e alertas para picos de 429 e quedas de Redis.
Implementar rate limiting é tanto projeto técnico quanto escolha de trade-offs: políticas mais rígidas protegem recursos, mas podem impactar clientes legítimos. Adote observabilidade e itere.
Este artigo forneceu o fluxo completo: por que usar token bucket, como configurar, exemplo errado e a correção com Redis, e um mini-projeto pronto para testes. Adapte valores de rate/capacity ao seu cenário e sempre documente as políticas para times de produto e infra.
Assuntos: spring-boot, NoSQL, rate-limiting, token-bucket, Java
Leia também
Spring Boot 3: habilitando Virtual Threads para APIs REST bloqueantes
Guia prático para habilitar virtual threads em APIs REST bloqueantes com Spring Boot 3: configuração do executor, exemplos com CompletableFuture…
Virtual Threads no Spring Boot: crie uma API concorrente passo a passo
Aprenda a usar virtual threads com Spring Boot para criar APIs concorrentes sem reescrever tudo. Veja code-review: o jeito errado, o certo, configuração…
Virtual Threads no Spring Boot: migrar controllers bloqueantes, JPA e tuning do HikariCP
Guia prático para migrar controllers bloqueantes e JPA para virtual threads no Spring Boot, com exemplos reais, setup do HikariCP, armadilhas e padrões para…
Idempotência em APIs Spring Boot: chaves, Redis e exemplo passo a passo
Implementação prática de idempotência em APIs Spring Boot usando chaves e Redis. Tutorial passo a passo com configuração, interceptor, controller e dicas para…
JPA Embeddables: criar e usar Value Objects (Address, Money) passo a passo
Aprenda passo a passo a criar e usar JPA Embeddables para value objects (Address, Money). Exemplos Java, boas práticas, armadilhas comuns e código funcional…
Multi-tenant em Spring Boot: schema-per-tenant com Hibernate, Flyway e routing
Implementação prática de schema-per-tenant em Spring Boot: routing do tenant, configuração Hibernate (MultiTenantConnectionProvider), e aplicação de migrações…