← voltar ao índice

· 7 min de leitura · por Tiago

Rate Limiting em APIs ASP.NET Core: Protegendo seus Endpoints com Elegância

Se você já colocou uma API em produção, provavelmente já se deparou com o cenário: um cliente mal configurado (ou mal-intencionado) começa a disparar centenas de requisições por segundo, e de repente seu banco de dados, sua fila ou seu serviço downstream começam a sofrer. Rate limiting (ou limitação de taxa) é uma das ferramentas mais simples e eficazes para evitar que isso aconteça, e desde o .NET 7 o ASP.NET Core traz suporte nativo para isso, sem precisar de bibliotecas de terceiros.

Neste post vamos entender o que é rate limiting, por que ele importa, quais algoritmos o ASP.NET Core oferece e como configurá-los na prática, incluindo dicas para ambientes com múltiplas instâncias rodando atrás de um load balancer.

Por que limitar a taxa de requisições?

Rate limiting existe para proteger recursos finitos. Isso pode significar:

  • Proteger a infraestrutura: evitar que um único cliente consuma toda a capacidade do servidor, do banco de dados ou de uma API de terceiros que você consome.
  • Garantir uso justo (fair use): quando múltiplos clientes compartilham a mesma API, ninguém deveria conseguir monopolizar os recursos.
  • Controlar custos: se sua API depende de serviços cobrados por chamada (como um provedor de IA ou uma API de geolocalização), limitar o consumo evita surpresas na fatura.
  • Mitigar ataques: rate limiting é uma camada a mais de defesa contra ataques de força bruta, scraping agressivo ou tentativas de negação de serviço.

Vale lembrar que rate limiting na aplicação não substitui uma camada de proteção na borda (como um API Gateway, CDN ou WAF), mas é um complemento importante, especialmente para regras de negócio mais específicas: por exemplo, limitar por usuário autenticado, e não apenas por IP.

O middleware nativo do ASP.NET Core

A partir do .NET 7, o namespace Microsoft.AspNetCore.RateLimiting traz um middleware completo de rate limiting, construído sobre o System.Threading.RateLimiting. Isso significa que você não precisa mais recorrer a pacotes externos para os casos mais comuns.

O registro básico é feito no Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    options.AddFixedWindowLimiter("padrao", limiterOptions =>
    {
        limiterOptions.PermitLimit = 100;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        limiterOptions.QueueLimit = 10;
    });
});

var app = builder.Build();

app.UseRateLimiter();

app.MapGet("/produtos", () => "Lista de produtos")
   .RequireRateLimiting("padrao");

app.Run();

Note dois pontos importantes: o middleware precisa ser adicionado ao pipeline com UseRateLimiter(), e cada endpoint (ou grupo de endpoints) escolhe explicitamente qual política aplicar através de RequireRateLimiting.

Os algoritmos disponíveis

O ASP.NET Core oferece quatro estratégias diferentes, cada uma adequada para um cenário específico.

Fixed Window (Janela Fixa)

É o mais simples e intuitivo: você define um número máximo de requisições permitidas dentro de um intervalo de tempo fixo (por exemplo, 100 requisições por minuto). Quando a janela termina, o contador zera. A desvantagem é o "efeito de borda": um cliente pode enviar 100 requisições no último segundo de uma janela e mais 100 no primeiro segundo da próxima, dobrando a taxa efetiva por um breve instante.

Sliding Window (Janela Deslizante)

Resolve o problema de borda do Fixed Window dividindo a janela em segmentos menores. Conforme o tempo avança, os segmentos mais antigos "expiram" gradualmente, suavizando o limite ao longo do tempo. É uma boa escolha quando você quer uma limitação mais justa sem o custo computacional de algoritmos mais sofisticados.

options.AddSlidingWindowLimiter("sliding", limiterOptions =>
{
    limiterOptions.PermitLimit = 100;
    limiterOptions.Window = TimeSpan.FromMinutes(1);
    limiterOptions.SegmentsPerWindow = 4;
    limiterOptions.QueueLimit = 5;
});

Token Bucket

Aqui o modelo é diferente: existe um "balde" com um número fixo de tokens, e cada requisição consome um token. Os tokens são reabastecidos periodicamente até um limite máximo. Esse algoritmo é ótimo para permitir rajadas (bursts) de tráfego ocasionais, desde que a média ao longo do tempo se mantenha dentro do esperado, algo muito comum em APIs que atendem clientes com padrões de uso irregulares.

options.AddTokenBucketLimiter("token", limiterOptions =>
{
    limiterOptions.TokenLimit = 50;
    limiterOptions.TokensPerPeriod = 10;
    limiterOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
    limiterOptions.QueueLimit = 5;
});

Concurrency Limiter

Diferente dos anteriores, esse não limita quantidade de requisições ao longo do tempo, mas quantas requisições podem ser processadas simultaneamente. É especialmente útil para proteger endpoints que fazem operações caras (como geração de relatórios ou chamadas a serviços lentos), onde o problema não é a frequência, mas a concorrência.

options.AddConcurrencyLimiter("concorrencia", limiterOptions =>
{
    limiterOptions.PermitLimit = 10;
    limiterOptions.QueueLimit = 5;
});

Particionando limites por usuário, IP ou chave de API

Aplicar o mesmo limite globalmente costuma ser insuficiente. O mais comum é particionar o rate limiting por cliente, seja pelo IP, por um header de API Key ou pelo identificador do usuário autenticado. Para isso, usamos AddPolicy com uma função de particionamento:

options.AddPolicy("por-usuario", context =>
{
    var usuarioId = context.User.Identity?.Name ?? "anonimo";

    return RateLimitPartition.GetFixedWindowLimiter(usuarioId, _ =>
        new FixedWindowRateLimiterOptions
        {
            PermitLimit = 20,
            Window = TimeSpan.FromMinutes(1)
        });
});

Dessa forma, cada usuário (ou IP) tem seu próprio "balde" de requisições, evitando que um cliente abusivo afete os limites de outro.

Comunicando o limite ao cliente

Uma boa API não apenas bloqueia requisições excedentes: ela informa o cliente sobre o que aconteceu. Ao rejeitar uma requisição, é uma boa prática retornar o status 429 Too Many Requests junto com um header Retry-After, indicando quando o cliente pode tentar novamente:

options.OnRejected = async (context, token) =>
{
    context.HttpContext.Response.Headers.RetryAfter = "60";

    await context.HttpContext.Response.WriteAsync(
        "Você excedeu o limite de requisições. Tente novamente em instantes.",
        token);
};

Isso melhora significativamente a experiência de quem consome sua API, permitindo que clientes implementem retry com backoff de forma inteligente em vez de simplesmente martelar o endpoint novamente.

Rate limiting em ambientes com múltiplas instâncias

Um detalhe crucial: o rate limiter nativo do ASP.NET Core mantém seu estado em memória, por instância. Isso funciona perfeitamente para uma única instância da aplicação, mas em ambientes distribuídos (como um cluster no Kubernetes com múltiplos pods rodando atrás de um load balancer), cada instância terá seu próprio contador independente. Na prática, isso significa que o limite efetivo é multiplicado pelo número de instâncias.

Para resolver isso, existem algumas abordagens:

  1. Delegar para a borda: usar um API Gateway (como o YARP, Azure API Management, ou um gateway de nuvem) que aplique rate limiting de forma centralizada antes do tráfego chegar às instâncias da aplicação.
  2. Estado compartilhado com Redis: implementar (ou adotar bibliotecas que implementem) um RateLimiter customizado que armazene os contadores em um cache distribuído, garantindo que todas as instâncias compartilhem o mesmo estado.
  3. Aceitar a aproximação: em muitos cenários, um limite "aproximado" por instância já é suficiente para proteger a infraestrutura, mesmo sem coordenação perfeita entre elas.

A escolha depende da criticidade do controle. Se o objetivo é apenas evitar abuso grosseiro, a aproximação por instância costuma bastar. Se o requisito é um limite de negócio rígido (como um plano de API que garante exatamente X chamadas por minuto), vale investir em uma solução distribuída.

Testando sua configuração

Antes de subir para produção, vale simular o comportamento sob carga. Ferramentas como k6, bombardier ou até um script simples com curl em loop ajudam a validar se os limites estão sendo aplicados corretamente, se o status 429 está sendo retornado como esperado e se o header Retry-After faz sentido. Também é importante testar o comportamento da fila (quando QueueLimit é maior que zero), garantindo que requisições enfileiradas não acabem estourando timeouts do cliente.

Conclusão

O rate limiting deixou de ser um recurso "extra" que exigia bibliotecas externas e passou a ser parte do arsenal padrão do ASP.NET Core. Com poucas linhas de código é possível escolher entre janelas fixas, deslizantes, token bucket ou controle de concorrência, além de particionar limites por usuário, IP ou qualquer outra chave que faça sentido para o seu domínio.

O ponto de atenção mais importante é lembrar que o estado do limitador é local à instância: em arquiteturas distribuídas, complemente essa proteção com um gateway centralizado ou um armazenamento compartilhado, especialmente quando o limite for parte de um contrato comercial com seus clientes. De resto, adicionar rate limiting às suas APIs é uma daquelas melhorias de baixo custo e alto impacto que toda aplicação em produção deveria ter.

← voltar ao índice