Renan Montanari Blog
Voltar ao blog
SegurançaAPI

Rate Limiting: A Diferença Entre API Protegida e API Vulnerável

Bots, integrações mal feitas, loops sem querer e ataques: sem limite de requisições, um único cliente consegue deixar o sistema lento para todo mundo. Como implementar rate limiting do jeito certo.

Renan Montanari

Renan Montanari

Engenheiro de Software Full-Stack

10 min de leitura

Introdução: Quando Sua API Vira Arma Contra Você

Seu sistema está em pé há meses. Tudo funcionando. Aí um dia, sem motivo aparente, tudo fica lento.

Não é erro. Não é acidente. É alguém, ou alguma coisa, fazendo requisições tão rápido que sua API não consegue respirar.

Pode ser:

  • Um bot testando vulnerabilidades
  • Um cliente com integração ruim que dispara 100 requisições em paralelo
  • Um script de scraping copiando seu catálogo
  • Um estagiário rodando um loop infinito sem querer
  • Um ataque deliberado

E aqui está o problema: sua API tentou responder a todas as requisições.

Os usuários legítimos? Ficaram esperando na fila.

A diferença entre uma API profissional e uma amadora é simples: a profissional diz "não" quando precisa. A amadora tenta responder tudo e acaba falhando para todo mundo.

Por Que Ninguém Implementa Rate Limiting No Começo

O Cenário Comum

Você começa. A API é rápida. 100 usuários, tudo certo. Sem rate limiting, porque:

  1. "Por enquanto funciona": não tem lentidão, então não precisa de limite
  2. "É complexo": parece coisa de empresa grande
  3. "Não dá tempo": sprint cheio, fica para depois
  4. "Ninguém vai atacar": confiança demais

Aí vocês chegam a 1.000 usuários. A integração ruim de um cliente começa a fazer 1.000 requisições por minuto. E todo mundo congela.

O Que Pode Acontecer Sem Rate Limiting

Cenário 1: Bot de Scraping

Um bot começa a chamar seu endpoint de produtos. 10 por segundo. 100. 1.000.

Seu servidor tenta responder a cada um. E tenta. E tenta.

Conexões ficam presas. A memória sobe. A CPU dispara. O banco de dados começa a recusar conexões porque atingiu o limite.

O usuário legítimo tenta entrar. Timeout. Tudo congela.

Cenário 2: Integração Ruim

Um cliente integra sua API e faz uma requisição a cada tecla que o usuário dele digita.

Usuário digitando rápido? 10 requisições por segundo.

Multiplica por 500 usuários do cliente ao mesmo tempo: 5.000 requisições por segundo.

Seu banco foi dimensionado para 500.

Tudo fica lento. Não só para aquele cliente: para todo mundo.

Cenário 3: Ataque Deliberado

Alguém aponta centenas de máquinas para o seu sistema, cada uma fazendo milhares de requisições por segundo.

Sem nenhuma camada de proteção, seu servidor tenta responder a cada uma.

Você fica fora do ar por horas.

A Solução: Rate Limiting Bem Feito

Rate limiting não é "bloquear usuário". É distribuir de forma justa.

É você dizer: "cada usuário pode fazer X requisições a cada Y segundos. Passou disso, recebe 429 Too Many Requests e tenta de novo depois."

1️⃣ O Padrão: Token Bucket

A ideia é simples: cada usuário tem um balde de fichas. Cada requisição gasta uma ficha. O balde se reabastece num ritmo constante. Balde vazio, requisição recusada.

Isso permite rajadas curtas (o balde cheio) sem permitir abuso contínuo (o ritmo de reposição).

// ✅ Algoritmo Token Bucket (em memória)

interface Bucket {
  tokens: number; // fichas disponíveis agora
  lastRefill: number; // timestamp da última reposição
}

class TokenBucketLimiter {
  private buckets = new Map<string, Bucket>();

  constructor(
    private capacity = 100, // rajada máxima: 100 requisições seguidas
    private refillPerSecond = 1, // depois disso, 1 por segundo (60/min)
  ) {}

  isAllowed(userId: string): boolean {
    const now = Date.now();
    const bucket = this.buckets.get(userId) ?? {
      tokens: this.capacity, // primeiro acesso: balde cheio
      lastRefill: now,
    };

    // Repõe as fichas proporcionais ao tempo que passou
    const secondsPassed = (now - bucket.lastRefill) / 1000;
    bucket.tokens = Math.min(
      this.capacity,
      bucket.tokens + secondsPassed * this.refillPerSecond,
    );
    bucket.lastRefill = now;
    this.buckets.set(userId, bucket);

    if (bucket.tokens >= 1) {
      bucket.tokens -= 1; // gasta uma ficha
      return true;
    }

    return false; // balde vazio: recusa
  }
}

// Uso
const limiter = new TokenBucketLimiter(100, 1);

app.get("/api/products", (req, res) => {
  if (!limiter.isAllowed(req.user.id)) {
    res.setHeader("Retry-After", "1");
    return res.status(429).json({
      error: "Too Many Requests",
      message: "Você fez requisições demais em pouco tempo. Tente novamente em instantes.",
    });
  }

  res.json(products);
});

Serve para entender o conceito e para sistemas com um único servidor. Mas tem dois limites: o Map cresce para sempre (precisa de limpeza periódica) e cada servidor tem sua própria contagem.

2️⃣ Rate Limiting com Redis (Produção)

Com três servidores atrás de um load balancer, o limite em memória vira três limites. O usuário que deveria fazer 100 requisições faz 300.

Solução: guardar a contagem num lugar compartilhado. Redis é a escolha natural.

import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

type RateLimitResult = {
  allowed: boolean;
  limit: number;
  remaining: number;
  resetSeconds: number;
};

class RedisRateLimiter {
  constructor(
    private name: string, // separa os contadores de cada regra
    private capacity = 100,
    private windowMs = 60_000, // 1 minuto
  ) {}

  async check(identifier: string): Promise<RateLimitResult> {
    const key = `ratelimit:${this.name}:${identifier}`;

    // Incrementa e lê o tempo restante numa única ida ao Redis
    const [[, count], [, ttl]] = (await redis
      .multi()
      .incr(key)
      .pttl(key)
      .exec()) as [[null, number], [null, number]];

    // Chave sem expiração (primeira requisição da janela):
    // define agora. Se isso falhar, a próxima requisição corrige,
    // e a chave nunca fica "eterna" bloqueando o usuário
    if (ttl === -1) {
      await redis.pexpire(key, this.windowMs);
    }

    const resetMs = ttl > 0 ? ttl : this.windowMs;

    return {
      allowed: count <= this.capacity,
      limit: this.capacity,
      remaining: Math.max(0, this.capacity - count),
      resetSeconds: Math.ceil(resetMs / 1000),
    };
  }
}

// Middleware reutilizável
const rateLimit =
  (limiter: RedisRateLimiter, getId: (req: Request) => string) =>
  async (req: Request, res: Response, next: NextFunction) => {
    const result = await limiter.check(getId(req));

    // Headers no padrão popularizado pela API do GitHub
    res.setHeader("X-RateLimit-Limit", result.limit);
    res.setHeader("X-RateLimit-Remaining", result.remaining);
    res.setHeader(
      "X-RateLimit-Reset",
      Math.floor(Date.now() / 1000) + result.resetSeconds,
    );

    if (!result.allowed) {
      res.setHeader("Retry-After", result.resetSeconds);
      return res.status(429).json({
        error: "Too Many Requests",
        message: "Você atingiu o limite de requisições.",
        retryAfter: result.resetSeconds,
      });
    }

    next();
  };

app.use(
  "/api/",
  rateLimit(new RedisRateLimiter("api", 100, 60_000), (req) => req.user.id),
);

Esse é um contador por janela fixa, mais simples que o token bucket. A troca: na virada da janela, o usuário consegue até o dobro do limite em poucos segundos. Para a maioria dos sistemas, é aceitável. Se não for, o token bucket também pode rodar no Redis com um script Lua, ou você pode usar uma biblioteca pronta como rate-limiter-flexible.

3️⃣ Limites Diferentes Por Endpoint

Nem todo endpoint merece o mesmo limite.

  • Login: limite baixo (proteção contra tentativa de senha em massa)
  • Listar produtos: limite alto (leitura é barata)
  • Pagamento e exportação: limite baixo (operação cara)
const limits = {
  login: new RedisRateLimiter("login", 5, 60_000), // 5/min
  register: new RedisRateLimiter("register", 3, 3_600_000), // 3/hora
  products: new RedisRateLimiter("products", 1000, 60_000), // 1000/min
  charge: new RedisRateLimiter("charge", 10, 60_000), // 10/min
  export: new RedisRateLimiter("export", 3, 3_600_000), // 3/hora
};

// Login e cadastro acontecem ANTES de existir usuário autenticado:
// o identificador é o IP (e, no login, também o e-mail tentado)
app.post(
  "/auth/login",
  rateLimit(limits.login, (req) => `${req.ip}:${req.body.email}`),
);
app.post("/auth/register", rateLimit(limits.register, (req) => req.ip));

// Rotas autenticadas: o identificador é o usuário
app.get("/api/products", rateLimit(limits.products, (req) => req.user.id));
app.post("/api/payment/charge", rateLimit(limits.charge, (req) => req.user.id));
app.post("/api/export", rateLimit(limits.export, (req) => req.user.id));

Dois detalhes que costumam passar batido:

  • Cada regra tem seu próprio contador. Se o limite de login e o de produtos usarem a mesma chave no Redis, navegar pelo catálogo gasta tentativas de login.
  • Na tela de login não existe req.user. Limitar por usuário ali não protege nada. O limite precisa ser por IP e por conta alvo.

4️⃣ Limite Por IP: Uma Camada a Mais

Limite por usuário não segura quem nem chegou a fazer login. Um limite geral por IP pega bots e scripts antes de qualquer outra coisa.

// Necessário se a API está atrás de proxy, CDN ou load balancer,
// senão req.ip será sempre o IP do proxy
app.set("trust proxy", 1);

app.use(
  rateLimit(new RedisRateLimiter("ip", 1000, 60_000), (req) => req.ip),
);

Cuidados com limite por IP:

  • Muita gente pode dividir o mesmo IP. Empresas, universidades e operadoras de celular colocam centenas de pessoas atrás de um único endereço. Seja generoso aqui.
  • trust proxy mal configurado é pior que nada. Ou todo mundo vira um IP só, ou o atacante forja o header X-Forwarded-For e escapa do limite.

E o Ataque de Verdade?

Uma ressalva honesta: rate limiting na aplicação não segura um ataque distribuído de grande porte. Quando milhares de máquinas atacam ao mesmo tempo, cada requisição ainda chega ao seu servidor, abre conexão e consome recurso, mesmo que seja para responder 429.

Contra isso, a proteção precisa estar antes da sua aplicação: CDN e WAF (Cloudflare, AWS WAF e similares), que absorvem o volume na borda da rede.

O rate limiting da aplicação resolve o resto, que na prática é a maior parte do dia a dia: integração mal feita, bot de scraping, força bruta no login, script em loop.

Trade-offs: Proteção vs Experiência

Limite Muito Baixo

✅ Protege contra abuso ❌ Usuários legítimos ficam frustrados

Limite Muito Alto

✅ Ninguém fica bloqueado ❌ Um único cliente ainda consegue derrubar o sistema

Solução: limites por plano

const limitsByPlan = {
  free: 100, // por minuto
  pro: 10_000,
  enterprise: null, // definido em contrato, por cliente
};

const capacity = limitsByPlan[user.plan] ?? user.customLimit ?? 100;

De quebra, o limite vira argumento de venda: quem precisa de mais, paga mais.

Implementação: Uma Tarde

Hora 1:

  • Subir o Redis
  • Implementar o limitador (ou adotar uma biblioteca pronta)

Hora 2:

  • Criar o middleware
  • Aplicar nos endpoints críticos: login, cadastro, pagamento, exportação

Hora 3:

  • Testar com ab (Apache Bench) ou k6
  • Ajustar os limites com base no teste

Hora 4:

  • Alerta quando o volume de respostas 429 subir (veja o post sobre monitoramento)
  • Painel com os clientes que mais batem no limite
# Teste de carga rápido
ab -n 1000 -c 100 http://localhost:3000/api/products

# Resultado esperado: parte das requisições respondida com 429

Checklist: Você Está Protegido?

  • Tem rate limiting implementado?
  • Os limites variam por endpoint?
  • Tem limite por IP, além do limite por usuário?
  • O contador é compartilhado entre servidores (Redis ou similar)?
  • As respostas informam o limite restante e o Retry-After?
  • Monitora o volume de respostas 429?
  • Fez teste de carga?
  • Login e cadastro têm limites baixos, por IP e por conta?
  • Operações caras (pagamento, exportação) têm limites baixos?
  • Tem CDN ou WAF na frente para ataques de grande volume?

Menos de 6 marcados = você está em risco.

O Medo Real: Bloquear Cliente Legítimo

Quando você implementa rate limiting, aparece um medo:

"E se um cliente legítimo bater no limite?"

Vai acontecer. Um cliente integra sua API, o código dele tem um bug que dispara requisições em paralelo, e o limite entra em ação.

E isso é bom. O bug dele afeta só ele, e não todos os seus outros clientes.

O que faz diferença é a comunicação:

{
  "error": "Too Many Requests",
  "message": "Você atingiu o limite de 100 requisições por minuto.",
  "retryAfter": 45,
  "documentation": "https://docs.sua-api.com/rate-limiting",
  "support": "suporte@sua-api.com"
}

Avise, explique, ofereça suporte. E monitore quem bate no limite com frequência: muitas vezes é um cliente que precisa de ajuda na integração, ou de um plano maior.

Próximos Passos

Se você deixou mais de 4 itens do checklist em branco:

Sua API está vulnerável.

Uma tarde implementando rate limiting economiza:

  • Horas fora do ar por abuso ou ataque
  • Clientes legítimos prejudicados por um único cliente mal comportado
  • A investigação caótica de "por que tudo ficou lento?"

Quer Aprofundar?

Se você está escalando seu sistema e quer garantir que nenhuma requisição maliciosa (ou mal planejada) derrube tudo, posso ajudar com:

  • Auditoria de Segurança da API: mapeamento de vulnerabilidades e teste de carga
  • Rate Limiting Implementado: por usuário, por IP e por endpoint
  • Proteção em Camadas: CDN, WAF e limites na aplicação trabalhando juntos
  • Monitoramento e Alertas: detectar abuso antes de virar problema

Vamos proteger sua API →

Gostou? Compartilhe