[CS] CARLOS SEIDL EN
Artigos

Rate Limiting em .NET: Controlando o Tráfego da Sua API

Proteja sua API de abuso e picos de tráfego com o rate limiting nativo do ASP.NET Core: 4 algoritmos, resposta 429 com Retry-After e exemplo em .NET 10.

Por 15 min de leitura
  • Rate Limiting
  • ASP.NET Core
  • APIs
  • .NET 10
Capa do artigo: Rate Limiting em .NET: Controlando o Tráfego da Sua API

Rate limiting: como a API se protege de quem a chama

Sem limite, um cliente abusivo prejudica todos; com limite, o excesso é recusado
Sem limite, um cliente abusivo prejudica todos; com limite, o excesso é recusado

Toda API pública ou interna tem uma capacidade finita. Um cliente com um loop sem controle, um script de força bruta ou um pico de acessos legítimo pode consumir CPU, conexões e banco até a API ficar lenta ou cair para todos os outros clientes. No artigo anterior (Tolerância a Falhas vs Resiliência) vimos como o chamador se protege de uma dependência com problema. Aqui olhamos o lado oposto: como o servidor se protege de quem o chama.

O rate limiting (limitação de taxa) controla quantas requisições um cliente pode fazer em um intervalo de tempo e recusa o excedente, normalmente com HTTP 429 Too Many Requests. Desde o .NET 7, o ASP.NET Core traz um middleware nativo para isso, com os principais algoritmos prontos. Neste artigo cobrimos os conceitos e construímos uma API em .NET 10 que usa os quatro algoritmos, limites por cliente com planos diferentes, resposta 429 padronizada e uma demonstração que mostra cada comportamento.

Assista ao vídeo

Limite de 5, passaram 10: Rate Limiting no .NET
Vídeo no canal ForgeTechPit: Limite de 5, passaram 10: Rate Limiting no .NET

Conceitos de rate limiting e algoritmos nativos

Rate limiting: limitar a quantidade de requisições que um cliente (identificado por uma chave) pode fazer em um período. Passou do limite, a requisição é recusada antes de executar a regra de negócio. É uma decisão barata, tomada na borda.

Por que precisamos dele:

  • Abuso da API: consumo muito acima do uso normal.
  • Tráfego tipo DDoS: muitas requisições de muitas origens (o rate limiting ajuda, mas não substitui proteção de rede).
  • Picos acidentais: um cliente com bug ou retry sem controle.
  • Esgotamento de recursos: banco, conexões e CPU são finitos.
  • Força bruta: tentativas repetidas de login ou de adivinhar tokens.
Como funciona: a requisição passa pelo limitador antes do endpoint
Como funciona: a requisição passa pelo limitador antes do endpoint

Os quatro algoritmos nativos

  • Janela fixa (fixed window): conta as requisições em intervalos fixos (por exemplo, 5 a cada 10 s). Simples e barata, mas permite uma rajada na virada da janela.
  • Janela deslizante (sliding window): divide a janela em segmentos que "escorregam" com o tempo, suavizando o pico da virada ao custo de um pouco mais de memória.
  • Token bucket: um balde com capacidade de N fichas, que são repostas em ritmo constante. Cada requisição consome uma ficha. Permite rajadas controladas (até esvaziar o balde) e depois impõe o ritmo médio.
  • Concorrência (concurrency): limita quantas requisições estão em andamento ao mesmo tempo, não a taxa. Protege operações pesadas, como relatórios e exportações.
Os quatro algoritmos: janela fixa, deslizante, token bucket e concorrência
Os quatro algoritmos: janela fixa, deslizante, token bucket e concorrência

O problema da janela fixa merece atenção. Se o limite é 5 a cada 10 s, um cliente pode mandar 5 requisições aos 9 s e mais 5 aos 11 s: 10 requisições em 2 segundos, o dobro do pretendido. A janela deslizante evita isso porque sempre olha os últimos 10 s, qualquer que seja o instante.

O problema da virada da janela fixa e como a janela deslizante o evita
O problema da virada da janela fixa e como a janela deslizante o evita

Dá para reproduzir isso direto com System.Threading.RateLimiting, sem subir uma API. O trecho a seguir (ilustrativo, fora do repositório) usa uma janela de 2 s em vez de 10 s só para o teste ser rápido: dispara 5 requisições no fim da primeira janela e 5 no começo da seguinte, nos dois algoritmos:

C#
using System.Threading.RateLimiting;





var janela = TimeSpan.FromSeconds(2);





RateLimiter fixa = new FixedWindowRateLimiter(


    new FixedWindowRateLimiterOptions


    {


        PermitLimit = 5, Window = janela, QueueLimit = 0


    });





RateLimiter deslizante = new SlidingWindowRateLimiter(


    new SlidingWindowRateLimiterOptions


    {


        PermitLimit = 5, Window = janela,


        SegmentsPerWindow = 4, QueueLimit = 0


    });





static int Rajada(RateLimiter limitador) =>


    Enumerable.Range(0, 5)


        .Count(_ => limitador.AttemptAcquire().IsAcquired);





static void Mostrar(string t, RateLimiter f, RateLimiter d) =>


    Console.WriteLine($"t={t} s  fixa: {Rajada(f)}/5  "


        + $"deslizante: {Rajada(d)}/5");





await Task.Delay(1500);   // fim da primeira janela


Mostrar("1,5", fixa, deslizante);


await Task.Delay(700);    // começo da janela seguinte


Mostrar("2,2", fixa, deslizante);

A saída (os tempos dependem do relógio, mas o padrão se repete):

Texto
t=1,5 s  fixa: 5/5  deslizante: 5/5


t=2,2 s  fixa: 5/5  deslizante: 0/5

A janela fixa aceitou 10 requisições em 0,7 s (5/5 nos dois instantes), porque o contador zerou na virada. A deslizante ainda enxergava as 5 do instante anterior dentro da janela e recusou todas (0/5). O AttemptAcquire não espera nem enfileira: devolve na hora se a requisição passou, que é o que o middleware faz por baixo.

Limitar por quê: a chave da partição

O limite vale por chave: cada chave tem seu próprio contador. A escolha da chave define o comportamento:

  • Por IP: simples, mas vários usuários atrás do mesmo NAT dividem a cota.
  • Por usuário: justo, mas exige autenticação antes do limitador.
  • Por API key: permite planos diferentes (gratuito e premium, por exemplo).
  • Por endpoint: protege rotas caras sem punir as baratas.

Em código, a chave é só uma string devolvida pela política. Duas variantes (trecho ilustrativo, dentro do AddRateLimiter):

C#
options.AddPolicy("por-ip", http =>


    RateLimitPartition.GetFixedWindowLimiter(


        http.Connection.RemoteIpAddress?.ToString()


            ?? "anonimo",


        _ => Cota()));





options.AddPolicy("por-usuario", http =>


    RateLimitPartition.GetFixedWindowLimiter(


        http.User.Identity?.Name


            ?? http.Connection.RemoteIpAddress?.ToString()


            ?? "anonimo",


        _ => Cota()));





static FixedWindowRateLimiterOptions Cota() => new()


{


    PermitLimit = 2,


    Window = TimeSpan.FromMinutes(1)


};

A primeira conta tudo por IP; a segunda usa o usuário autenticado e cai para o IP quando não há login. Em um teste com os usuários ana e bia chegando do mesmo IP e cota de 2: na política por IP, ana, bia e ana de novo fizeram 3 requisições e a terceira recebeu 429, porque os dois dividiram a cota; na política por usuário, ana esgotou a dela na terceira chamada e bia ainda passou. Para o User.Identity estar preenchido, a autenticação precisa rodar antes do limitador.

A chave da partição define quem consome a cota de quem
A chave da partição define quem consome a cota de quem

Resposta ao estourar o limite

O padrão é responder 429 Too Many Requests com o cabeçalho Retry-After (em segundos), que diz ao cliente quando tentar de novo. Um cliente bem-comportado respeita esse valor, aplicando a mesma ideia de backoff que vimos no artigo anterior.

Rate limiting, throttling, quota e circuit breaker

Quatro termos que costumam ser misturados:

  • Rate limiting: limita a taxa em um intervalo curto e recusa o excedente (429).
  • Throttling: reduz a velocidade (atrasa, enfileira, degrada) em vez de recusar, para continuar atendendo com capacidade restrita.
  • Quota: teto de consumo em um período longo (por exemplo, 1 milhão de requisições por mês), ligado ao plano comercial. Pode conviver com rate limiting: 100 por minuto e 1 milhão por mês.
  • Circuit breaker: protege o chamador de uma dependência que está falhando. Rate limiting protege o servidor de quem chama.
Rate limiting e seus vizinhos: throttling, quota e circuit breaker
Rate limiting e seus vizinhos: throttling, quota e circuit breaker

Middleware de rate limiting no ASP.NET Core

No ASP.NET Core, o rate limiting é um middleware. Você registra as políticas com AddRateLimiter, ativa o middleware com UseRateLimiter e associa a política aos endpoints com RequireRateLimiting. A ordem importa: o UseRateLimiter precisa vir antes dos endpoints.

Dois níveis de limite se combinam:

  • GlobalLimiter: roda para toda requisição (por exemplo, 120 por minuto por IP). É a rede de segurança geral.
  • Política do endpoint: aplicada às rotas marcadas com RequireRateLimiting.

Se qualquer um dos dois recusar, a requisição é rejeitada. Um ponto único, OnRejected, padroniza a resposta (status, Retry-After e corpo). Rotas como o health check podem ser excluídas com DisableRateLimiting.

Rate limiting no pipeline do ASP.NET Core: limite global e política do endpoint
Rate limiting no pipeline do ASP.NET Core: limite global e política do endpoint

Atenção ao status de rejeição. Se você não configurar nada, o middleware responde 503 Service Unavailable ao rejeitar. Por isso o exemplo define o status 429 explicitamente no OnRejected.

A montagem mínima, com o status explícito, o middleware e uma política aplicada a um grupo de rotas (trecho ilustrativo; o exemplo completo está na seção 4):

C#
builder.Services.AddRateLimiter(o =>


{


    // Sem esta linha, a recusa responde 503


    o.RejectionStatusCode =


        StatusCodes.Status429TooManyRequests;





    o.AddFixedWindowLimiter("api", l =>


    {


        l.PermitLimit = 2;


        l.Window = TimeSpan.FromMinutes(1);


    });


});





var app = builder.Build();


app.UseRateLimiter();





// A política vale para o grupo; /saude fica de fora


var api = app.MapGroup("/api").RequireRateLimiting("api");


api.MapGet("/pedidos", () => "ok");


api.MapGet("/saude", () => "ok").DisableRateLimiting();

Chamando /api/pedidos três vezes: 200, 200 e 429. O /api/saude continuou respondendo 200. Sem a linha do RejectionStatusCode (e sem um OnRejected que defina o status), a terceira chamada respondeu 503 Service Unavailable. Repare também que RequireRateLimiting no grupo evita repetir a política em cada rota.

Ambiente distribuído

Os limitadores nativos guardam o contador em memória, por instância. Com 3 instâncias atrás de um balanceador e um limite de 100 por minuto, o cliente consegue até 300 por minuto, porque cada instância conta separadamente. Para um limite global exato há três caminhos:

  • Aplicar o limite no gateway (YARP, Azure API Management, Front Door/WAF), que enxerga todo o tráfego, como vimos no artigo de API Gateway + BFF.
  • Usar um armazenamento compartilhado (por exemplo, Redis) como contador.
  • Aceitar o limite aproximado por instância, quando a precisão não é crítica.
Contador por instância versus contador compartilhado
Contador por instância versus contador compartilhado

Rate limiting em C# com API e cliente de teste

O exemplo tem dois projetos: RateLimitApi (a API com os limitadores) e RateLimitDemo (um cliente que dispara requisições e mostra o resultado). O código completo está no repositório (link nas Referências). Começamos pela identificação do cliente, que define quem é limitado e quanto ele pode:

C#
namespace RateLimitApi;





public static class Clientes


{


    // EN: Defines WHO is limited (the partition key) and


    // EN: HOW MUCH they can do (the plan).


    // PT: Define QUEM é limitado (a chave da partição) e


    // PT: QUANTO ele pode (o plano).


    public static (string Chave, int Limite) Identificar(


        HttpContext http)


    {


        var chave = http.Request.Headers["X-Api-Key"].ToString();





        if (chave.StartsWith("premium-"))


            return (chave, 10);





        if (chave.Length > 0)


            return (chave, 3);





        var ip = http.Connection.RemoteIpAddress?.ToString();


        return ($"ip:{ip ?? "anonimo"}", 3);


    }


}

Quem manda uma chave premium- tem limite 10; qualquer outra chave, 3; sem chave, o limite é por IP. Em um sistema real, o plano viria do cadastro do cliente, não do prefixo da chave.

Agora a resposta padrão quando o limite estoura. Ela devolve 429, o Retry-After (quando o limitador informa) e um ProblemDetails:

C#
using System.Globalization;


using System.Threading.RateLimiting;


using Microsoft.AspNetCore.Mvc;


using Microsoft.AspNetCore.RateLimiting;





namespace RateLimitApi;





public static class Rejeicao


{


    // EN: When the limit is exceeded: 429 + Retry-After.


    // PT: Quando o limite estoura: 429 + Retry-After.


    public static async ValueTask Responder(


        OnRejectedContext contexto, CancellationToken ct)


    {


        var resposta = contexto.HttpContext.Response;





        if (contexto.Lease.TryGetMetadata(


                MetadataName.RetryAfter, out var espera))


        {


            var s = Math.Ceiling(espera.TotalSeconds);


            resposta.Headers.RetryAfter =


                s.ToString(CultureInfo.InvariantCulture);


        }





        var codigo = StatusCodes.Status429TooManyRequests;


        resposta.StatusCode = codigo;


        await resposta.WriteAsJsonAsync(new ProblemDetails


        {


            Status = codigo,


            Title = "Too many requests / Muitas requisições",


            Detail = "Limit exceeded. Try again later. / "


                + "Limite excedido. Tente de novo mais tarde."


        }, ct);


    }


}

As políticas concentram os quatro algoritmos, o limite global e a política por cliente:

C#
using System.Threading.RateLimiting;


using Microsoft.AspNetCore.RateLimiting;





namespace RateLimitApi;





public static class Politicas


{


    public const string Fixa = "fixa";


    public const string Deslizante = "deslizante";


    public const string Balde = "balde";


    public const string Concorrencia = "concorrencia";


    public const string PorCliente = "por-cliente";





    public static IServiceCollection AddPoliticasDeLimite(


        this IServiceCollection services) =>


        services.AddRateLimiter(options =>


        {


            options.OnRejected = Rejeicao.Responder;





            // EN: Global limit per IP: a general safety net.


            // PT: Limite global por IP: rede de segurança geral.


            options.GlobalLimiter = PartitionedRateLimiter


                .Create<HttpContext, string>(LimitePorIp);





            // EN: 1. Fixed window: 5 requests every 10 s.


            // PT: 1. Janela fixa: 5 requisições a cada 10 s.


            options.AddFixedWindowLimiter(Fixa, o =>


            {


                o.PermitLimit = 5;


                o.Window = TimeSpan.FromSeconds(10);


                o.QueueLimit = 0;


            });





            // EN: 2. Sliding window: same quota, but the window


            // EN:    is split into 5 segments that slide.


            // PT: 2. Janela deslizante: mesma cota, mas a janela


            // PT:    é dividida em 5 segmentos que deslizam.


            options.AddSlidingWindowLimiter(Deslizante, o =>


            {


                o.PermitLimit = 5;


                o.Window = TimeSpan.FromSeconds(10);


                o.SegmentsPerWindow = 5;


                o.QueueLimit = 0;


            });





            // EN: 3. Token bucket: burst of up to 5, refilled


            // EN:    by 1 token every 2 s.


            // PT: 3. Token bucket: rajada de até 5 e reposição


            // PT:    de 1 token a cada 2 s.


            options.AddTokenBucketLimiter(Balde, o =>


            {


                o.TokenLimit = 5;


                o.TokensPerPeriod = 1;


                o.ReplenishmentPeriod = TimeSpan.FromSeconds(2);


                o.QueueLimit = 0;


            });





            // EN: 4. Concurrency: at most 2 in flight.


            // PT: 4. Concorrência: no máximo 2 em andamento.


            options.AddConcurrencyLimiter(Concorrencia, o =>


            {


                o.PermitLimit = 2;


                o.QueueLimit = 0;


            });





            // EN: 5. Per client: each key has its own quota.


            // PT: 5. Por cliente: cada chave tem a sua própria


            // PT: cota.


            options.AddPolicy(PorCliente, http =>


            {


                var (chave, limite) = Clientes.Identificar(http);





                return RateLimitPartition.GetFixedWindowLimiter(


                    chave,


                    _ => new FixedWindowRateLimiterOptions


                    {


                        PermitLimit = limite,


                        Window = TimeSpan.FromSeconds(10),


                        QueueLimit = 0


                    });


            });


        });





    private static RateLimitPartition<string> LimitePorIp(


        HttpContext http)


    {


        var ip = http.Connection.RemoteIpAddress?.ToString();





        return RateLimitPartition.GetFixedWindowLimiter(


            ip ?? "anonimo",


            _ => new FixedWindowRateLimiterOptions


            {


                PermitLimit = 120,


                Window = TimeSpan.FromMinutes(1)


            });


    }


}

Pontos importantes:

  • Janela fixa e deslizante: a mesma cota (5 por 10 s). A deslizante divide a janela em 5 segmentos.
  • Token bucket: TokenLimit = 5 permite uma rajada de 5; TokensPerPeriod = 1 a cada 2 s repõe o ritmo.
  • Concorrência: no máximo 2 requisições em andamento, sem fila (QueueLimit = 0).
  • QueueLimit = 0: recusa na hora. Com valor maior, as requisições excedentes esperam em fila, o que se aproxima de throttling.
  • Por cliente: AddPolicy recebe o HttpContext e devolve uma partição. Cada chave ganha o próprio contador, com o limite do seu plano.
  • Limite global: PartitionedRateLimiter por IP, 120 por minuto, vale para todas as rotas.

O Program.cs da API associa cada política a um endpoint:

C#
using Microsoft.AspNetCore.RateLimiting;


using RateLimitApi;





var builder = WebApplication.CreateBuilder(args);


builder.Services.AddPoliticasDeLimite();





var app = builder.Build();





// EN: Order matters: the limiter must come before the endpoints.


// PT: A ordem importa: o limitador precisa vir antes dos


// PT: endpoints.


app.UseRateLimiter();





static IResult Ok(string politica) => Results.Ok(new


{


    politica,


    atendidoEm = DateTime.UtcNow.ToString("HH:mm:ss.fff")


});





app.MapGet("/fixa", () => Ok(Politicas.Fixa))


    .RequireRateLimiting(Politicas.Fixa);





app.MapGet("/deslizante", () => Ok(Politicas.Deslizante))


    .RequireRateLimiting(Politicas.Deslizante);





app.MapGet("/balde", () => Ok(Politicas.Balde))


    .RequireRateLimiting(Politicas.Balde);





app.MapGet("/lento", async () =>


    {


        await Task.Delay(500);


        return Ok(Politicas.Concorrencia);


    })


    .RequireRateLimiting(Politicas.Concorrencia);





app.MapGet("/cliente", () => Ok(Politicas.PorCliente))


    .RequireRateLimiting(Politicas.PorCliente);





// EN: Health check outside the global limit and the policies.


// PT: Health check fora do limite global e das políticas.


app.MapGet("/saude", () => Results.Ok("ok"))


    .DisableRateLimiting();





app.Run();

O endpoint /lento espera 500 ms para que as requisições se sobreponham e o limite de concorrência apareça. O /saude usa DisableRateLimiting, pois um health check não deve ser limitado.

O cliente de demonstração dispara as sequências e imprime os códigos de status:

C#
using Resposta = (int Codigo, double? Espera);





// EN: API address: first argument, then RATE_LIMIT_API_URL,


// EN: then the default.


// PT: Endereço da API: primeiro argumento, depois


// PT: RATE_LIMIT_API_URL, depois o padrão.


var baseUrl = args.FirstOrDefault()


    ?? Environment.GetEnvironmentVariable("RATE_LIMIT_API_URL")


    ?? "http://localhost:5080";





using var http = new HttpClient


{


    BaseAddress = new Uri(baseUrl)


};





async Task<Resposta> Chamar(string rota, string? chave = null)


{


    using var pedido = new HttpRequestMessage(


        HttpMethod.Get, rota);


    if (chave is not null)


        pedido.Headers.Add("X-Api-Key", chave);





    using var resposta = await http.SendAsync(pedido);


    var espera = resposta.Headers.RetryAfter?.Delta;





    return ((int)resposta.StatusCode, espera?.TotalSeconds);


}





async Task<Resposta[]> Sequencia(


    string rota, int vezes, string? chave = null)


{


    var resultados = new List<Resposta>();


    for (var i = 0; i < vezes; i++)


        resultados.Add(await Chamar(rota, chave));


    return [.. resultados];


}





void Mostrar(string titulo, Resposta[] respostas)


{


    var codigos = string.Join(


        ' ', respostas.Select(r => r.Codigo));


    Console.WriteLine($"{titulo,-62}{codigos}");





    var espera = respostas


        .FirstOrDefault(r => r.Espera > 0).Espera;


    if (espera is not null)


        Console.WriteLine($"{"",-62}Retry-After: {espera}s");


}





Console.WriteLine(


    "Waiting for the API at / "


    + $"Aguardando a API em {baseUrl} ...");


while (true)


{


    try { await Chamar("/saude"); break; }


    catch (HttpRequestException) { await Task.Delay(300); }


}





Console.WriteLine();


Mostrar("Fixed window / Janela fixa (5/10s) x7",


    await Sequencia("/fixa", 7));


Mostrar("Sliding window / Janela deslizante x7",


    await Sequencia("/deslizante", 7));


Mostrar("Token bucket x7", await Sequencia("/balde", 7));





await Task.Delay(TimeSpan.FromSeconds(4.5));


Mostrar("Token bucket after / após 4.5 s x3",


    await Sequencia("/balde", 3));





var paralelas = await Task.WhenAll(


    Enumerable.Range(0, 4).Select(_ => Chamar("/lento")));


Mostrar(


    "Concurrency / Concorrência (2), 4 parallel / paralelas",


    paralelas);





Mostrar("free-1 (3/10s) x5",


    await Sequencia("/cliente", 5, "free-1"));


Mostrar("free-2 (other client / outro cliente) x2",


    await Sequencia("/cliente", 2, "free-2"));


Mostrar("premium-1 (10/10s) x5",


    await Sequencia("/cliente", 5, "premium-1"));


Mostrar("/saude (no limit / sem limite) x3",


    await Sequencia("/saude", 3));

Com a API rodando (dotnet run --project src/RateLimitApi), executar dotnet run --project src/RateLimitDemo produz (saída reformatada para caber na página; o repositório usa mensagens em inglês e português):

Texto
Fixed window / Janela fixa (5/10s) x7


  200 200 200 200 200 429 429


  Retry-After: 10s


Sliding window / Janela deslizante x7


  200 200 200 200 200 429 429


Token bucket x7


  200 200 200 200 200 429 429


  Retry-After: 2s


Token bucket after / após 4.5 s x3


  200 200 429


  Retry-After: 2s


Concurrency / Concorrência (2), 4 parallel / paralelas


  200 429 200 429


free-1 (3/10s) x5


  200 200 200 429 429


  Retry-After: 10s


free-2 (other client / outro cliente) x2


  200 200


premium-1 (10/10s) x5


  200 200 200 200 200


/saude (no limit / sem limite) x3


  200 200 200
Resultado do exemplo: cada quadrado é uma requisição, verde 200 e vermelho 429
Resultado do exemplo: cada quadrado é uma requisição, verde 200 e vermelho 429

Lendo os resultados:

  1. Janela fixa: 5 passam, as 2 seguintes recebem 429 com Retry-After: 10.
  2. Janela deslizante: o mesmo resultado na rajada. Repare que, neste teste, ela não devolveu Retry-After: o limitador só informa o tempo de espera quando consegue calculá-lo, e nem todo algoritmo informa. Se o cliente depende do cabeçalho, defina um valor padrão no OnRejected.
  3. Token bucket: a rajada de 5 passa e as 2 seguintes são recusadas. Depois de 4,5 s, 2 fichas foram repostas: 2 requisições passam e a 3ª é recusada.
  4. Concorrência: das 4 chamadas paralelas, 2 passam e 2 são recusadas, pois só há 2 vagas.
  5. Por cliente: free-1 esgota a cota de 3, mas free-2 não é afetado (partição isolada), e premium-1 tem 10.
  6. /saude: fora do limitador.

Boas práticas de rate limiting: 429 e Retry-After

  • Configure o status 429 explicitamente: o padrão do middleware é 503, que sugere falha do servidor, e não excesso do cliente.
  • Envie Retry-After: sem ele, o cliente tende a repetir imediatamente. Cuide dos algoritmos que não informam o tempo.
  • Escolha a chave com cuidado: limitar só por IP penaliza usuários atrás de NAT; por usuário exige autenticação antes do limitador (UseAuthentication antes de UseRateLimiter).
  • Combine níveis: limite global por IP como rede de segurança, mais limites por cliente e por endpoint para as rotas caras e sensíveis (login, exportação).
  • Limites mais duros em rotas sensíveis: login e recuperação de senha precisam de limites bem menores que consultas comuns, contra força bruta.
  • Cuidado com QueueLimit alto: a fila mantém conexões abertas e pode virar o próprio problema. Em geral, recuse rápido.
  • Atrás de proxy ou balanceador, o IP é outro: configure os cabeçalhos encaminhados (X-Forwarded-For) com o middleware de forwarded headers, senão todos os clientes parecem ter o mesmo IP do proxy e dividem a mesma cota.
  • Multi-instância exige decisão: o contador nativo é local. Para limite global exato, aplique no gateway ou use armazenamento compartilhado.
  • Não confunda com autorização: rate limiting controla volume, não permissão.
  • Observe: registre e meça as rejeições (por política e por cliente). Muitos 429 para o mesmo cliente podem indicar abuso, ou um limite mal calibrado.
  • Teste os limites: um cliente automatizado como o do exemplo documenta o comportamento esperado e pega regressões.
  • Ensine os clientes: documente os limites e oriente a respeitar o Retry-After com backoff; um cliente bem comportado nunca deveria receber 429.

Três armadilhas que aparecem em produção, com a correção (trechos ilustrativos, validados à parte e fora do repositório).

Retry-After ausente em alguns algoritmos. O Rejeicao.cs do exemplo só envia o cabeçalho quando o limitador informa o tempo, e a janela deslizante não informa. A correção é um valor padrão:

C#
// Com metadado, usa o tempo informado; sem ele, o padrão


var segundos =


    contexto.Lease.TryGetMetadata(


        MetadataName.RetryAfter, out var espera)


        ? Math.Ceiling(espera.TotalSeconds)


        : 10;





resposta.Headers.RetryAfter =


    segundos.ToString(CultureInfo.InvariantCulture);

No teste, a janela fixa devolveu Retry-After: 60 (o tempo real até a próxima janela) e a deslizante devolveu 10, o padrão.

IP do proxy em vez do IP do cliente. Atrás de balanceador ou gateway, RemoteIpAddress é o do proxy, e todos os clientes dividem a mesma cota. Restaure o IP original antes do limitador:

C#
builder.Services.Configure<ForwardedHeadersOptions>(o =>


    o.ForwardedHeaders = ForwardedHeaders.XForwardedFor);





var app = builder.Build();





app.UseForwardedHeaders();   // antes do UseRateLimiter


app.UseRateLimiter();

Com cota de 2 por IP, três requisições com X-Forwarded-For 1.1.1.1, 1.1.1.1 e 2.2.2.2: sem UseForwardedHeaders a terceira recebeu 429 (todas pareciam vir do mesmo endereço); com ele, as três passaram. O teste veio de localhost, que o middleware confia por padrão; em produção, informe os proxies ou as redes confiáveis nas opções, para que um cliente qualquer não consiga forjar o cabeçalho.

Limite nomeado é um contador só. AddFixedWindowLimiter("nome", ...) cria um contador para todos os clientes: no teste, com limite 1, um segundo cliente (outro IP) já recebeu 429 depois do primeiro. Para limitar cada cliente, use AddPolicy com partição, como neste limite de login por IP:

C#
options.AddPolicy("login", http =>


    RateLimitPartition.GetFixedWindowLimiter(


        http.Connection.RemoteIpAddress?.ToString()


            ?? "anonimo",


        _ => new FixedWindowRateLimiterOptions


        {


            PermitLimit = 5,


            Window = TimeSpan.FromMinutes(1)


        }));





app.MapPost("/login", () => "ok").RequireRateLimiting("login");

Na sexta tentativa de 1.1.1.1 em um minuto veio 429 com Retry-After: 60, e 2.2.2.2 continuou passando. As primeiras quatro políticas do exemplo da seção 4 (janela fixa, deslizante, token bucket e concorrência) são globais de propósito, para a demonstração; só a por-cliente é particionada.

Quando usar rate limiting e qual algoritmo escolher

Use rate limiting quando:

  • A API é exposta à internet ou a muitos consumidores.
  • Há endpoints caros (relatórios, buscas) ou sensíveis (login, envio de e-mail).
  • Você precisa distribuir a capacidade de forma justa entre clientes ou vender planos.
  • Quer reduzir o impacto de bugs de clientes com retry sem controle.

Escolha o algoritmo conforme o caso:

  • Janela fixa: o caso mais simples, quando a rajada na virada é aceitável.
  • Janela deslizante: quando o limite precisa valer em qualquer intervalo.
  • Token bucket: quando rajadas curtas são legítimas e você quer controlar o ritmo médio.
  • Concorrência: quando o problema é o paralelismo, não a taxa (operações pesadas).

Evite, ou revise, quando:

  • O serviço é interno, de baixo volume e com poucos consumidores conhecidos: o custo de calibrar e operar pode superar o benefício.
  • Você espera que o rate limiting nativo resolva um ataque DDoS volumétrico: ele atua na aplicação, depois que o tráfego já chegou. Proteção de rede e WAF são outra camada.
  • Precisa de limite global exato em várias instâncias e só tem contador em memória.
  • Os limites não foram calibrados com dados reais: limites baixos demais bloqueiam usuários legítimos.

Conclusão: escolha da chave e do algoritmo

O rate limiting é a proteção do lado do servidor: antes de gastar recursos com uma requisição, decide-se se o cliente ainda tem direito a ela. O ASP.NET Core entrega os quatro algoritmos nativos (janela fixa, janela deslizante, token bucket e concorrência), partições por chave para dar a cada cliente a sua cota e um ponto único para padronizar a resposta 429.

A reflexão prática: escolher a chave e o algoritmo certos importa mais do que o número do limite. Defina quem será limitado, o que acontece na rajada, como o cliente é informado (429 e Retry-After) e onde o contador vive quando há várias instâncias. Combinado com o que vimos sobre resiliência no cliente (timeout, retry com backoff e circuit breaker), o servidor limita e o cliente respeita, e o sistema se mantém estável nos dois lados.

Referências de rate limiting em .NET

Leia também

Quer uma revisão da arquitetura do seu backend .NET? Conheça o serviço de backend e arquitetura .NET.

← todos os artigos