← todos os artigos

.NET

Implementando rate limiting em APIs ASP.NET Core

Aprenda a proteger APIs ASP.NET Core com o middleware integrado de rate limiting, policies nomeadas, partições por cliente, responses HTTP 429 e escolhas práticas de configuração.

Pipeline de requests de uma API ASP.NET Core com middleware de rate limiting rejeitando requests excedentes com HTTP 429

Rate limiting controla quantos requests uma API aceita durante um período específico. Ele é útil para APIs públicas, endpoints de autenticação, operações de alto custo e integrações em que um cliente não deve consumir todos os recursos disponíveis.

O ASP.NET Core oferece middleware integrado de rate limiting com algoritmos fixed window, sliding window, token bucket e concurrency. As policies podem ser globais ou atribuídas somente a endpoints selecionados. (learn.microsoft.com)

Por que rate limiting é importante

Um rate limiter pode ajudar uma API a:

  • impedir que um único cliente monopolize os recursos;
  • reduzir picos acidentais de tráfego;
  • proteger a capacidade de database, CPU e serviços externos;
  • aplicar limites diferentes para diferentes usuários ou planos;
  • retornar uma response previsível quando não houver capacidade disponível.

Rate limiting não é uma proteção completa contra DDoS. Grandes ataques distribuídos também devem ser tratados nos limites da infraestrutura, como CDN, firewall de aplicação web, API gateway ou serviço de proteção em nuvem. (learn.microsoft.com)

Configurando uma policy fixed window

O algoritmo fixed window permite um número definido de requests durante uma janela de tempo. Quando a janela termina, seu contador é redefinido.

A configuração de Program.cs a seguir aceita 20 requests por minuto e coloca em fila até dois requests adicionais:

using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);

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

    options.AddFixedWindowLimiter("api", limiterOptions =>
    {
        limiterOptions.PermitLimit = 20;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 2;
        limiterOptions.QueueProcessingOrder =
            QueueProcessingOrder.OldestFirst;
    });
});

var app = builder.Build();

app.UseRouting();
app.UseRateLimiter();

app.MapGet("/products", () => Results.Ok(new[]
{
    new { Id = 1, Name = "Keyboard" },
    new { Id = 2, Name = "Mouse" }
})).RequireRateLimiting("api");

app.Run();

PermitLimit define quantos requests são aceitos, enquanto Window define quando as permissões são repostas. QueueLimit controla quantos requests podem aguardar uma permissão. Defini-lo como zero rejeita imediatamente os requests excedentes.

Para policies específicas de endpoint, UseRateLimiter deve ser executado depois de UseRouting. (learn.microsoft.com)

Retornando uma response 429 útil

Por padrão, requests rejeitados podem receber o código de status HTTP 429 Too Many Requests. Um callback OnRejected permite que a API retorne uma response mais clara e, quando disponível, um header Retry-After.

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

    options.OnRejected = async (context, cancellationToken) =>
    {
        if (context.Lease.TryGetMetadata(
            MetadataName.RetryAfter,
            out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString();
        }

        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Too many requests. Please try again later."
        }, cancellationToken);
    };

    options.AddFixedWindowLimiter("api", limiterOptions =>
    {
        limiterOptions.PermitLimit = 20;
        limiterOptions.Window = TimeSpan.FromMinutes(1);
        limiterOptions.QueueLimit = 0;
    });
});

O valor de Retry-After ajuda os clientes a decidir quando outro request provavelmente terá sucesso. Ainda é importante que os clientes tratem responses 429, em vez de presumir que todos os requests serão aceitos.

Limitando cada cliente separadamente

Uma única policy compartilhada significa que todos os chamadores consomem a mesma cota de requests. O particionamento cria um limiter separado para cada usuário, chave de API, tenant ou endereço IP.

options.AddPolicy("per-client", httpContext =>
{
    var clientId =
        httpContext.User.Identity?.Name ??
        httpContext.Connection.RemoteIpAddress?.ToString() ??
        "anonymous";

    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: clientId,
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 10,
            Window = TimeSpan.FromMinutes(1),
            QueueLimit = 0,
            AutoReplenishment = true
        });
});

Aplique-a como qualquer outra policy nomeada:

app.MapPost("/orders", CreateOrder)
   .RequireRateLimiting("per-client");

Um ID de usuário autenticado ou uma chave de API controlada geralmente é uma chave de partição mais confiável do que um endereço IP. Aplicações atrás de proxies também devem configurar corretamente os headers encaminhados antes de depender do IP do cliente.

Escolhendo um algoritmo

Use fixed window quando uma cota simples for suficiente. Use sliding window para limites mais uniformes nas transições entre janelas. Use token bucket quando rajadas controladas forem aceitáveis. Use concurrency limiting quando a principal preocupação for quantas operações de alto custo são executadas simultaneamente, em vez de quantos requests chegam por minuto. (learn.microsoft.com)

Antes do deploy em produção, mantenha os limites na configuração, monitore os requests rejeitados e faça testes de carga com os valores selecionados. Um limite deve refletir o custo e a capacidade reais do endpoint protegido — não um número arbitrário copiado para toda a API.

Referências