Rate limiting costuma ser lembrado quando alguém fala de abuso, bots, scraping, brute force ou clientes fazendo chamadas demais. Essa leitura está certa, mas é incompleta. Em uma API bem desenhada, rate limit não serve apenas para bloquear excesso. Ele também comunica capacidade, protege dependências críticas e orienta o cliente sobre como se comportar quando está perto de consumir mais do que deveria.
Todo sistema tem limite, mesmo quando roda em cloud e usa autoscaling. O banco tem limite de conexões. O provider de pagamento tem limite contratual. O endpoint de busca consome CPU. A chamada para um modelo de IA consome tokens e dinheiro. Uma API de exportação pode parecer simples para o cliente, mas custar caro para o backend. Quando esses limites não são explícitos, eles continuam existindo; apenas aparecem mais tarde como timeout, fila acumulada, custo inesperado ou incidente.
É por isso que eu gosto de pensar em rate limiting como contrato de capacidade. A pergunta não é apenas “quantas chamadas devo bloquear?”. A pergunta melhor é: “quanto desta operação o sistema aceita, por consumidor, por intervalo e por custo, sem prejudicar o restante da plataforma?”. Essa mudança de perspectiva altera inclusive a forma como a API responde. Um bom rate limit não só rejeita. Ele explica.
O erro comum: responder 429 sem orientar o cliente
O status 429 Too Many Requests existe justamente para indicar que o cliente enviou requisições demais em determinado intervalo. O problema é que muitas APIs retornam 429 como se fosse um erro genérico, sem dizer quando tentar de novo, qual limite foi excedido ou se o cliente está em uma janela curta, quota diária ou restrição temporária. Isso força o consumidor a adivinhar.
Uma resposta ruim seria algo assim:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Too many requests"
}
Essa resposta até informa que houve excesso, mas não ajuda o cliente a se comportar melhor. O cliente não sabe se deve esperar 1 segundo, 30 segundos ou 1 hora. Não sabe se o limite é por usuário, tenant, IP ou aplicação. Não sabe se foi uma rajada curta ou se acabou a quota do dia. Em muitos casos, o resultado é pior: o cliente implementa retry imediato e aumenta a pressão sobre um sistema que já está tentando se proteger.
Uma resposta melhor usa cabeçalhos para transformar a rejeição em contrato. O mínimo esperado em um cenário de 429 é incluir Retry-After, indicando quanto tempo o cliente deve esperar antes de tentar novamente. Em APIs mais maduras, também faz sentido retornar informações de rate limit, como a política aplicada e a janela atual.
Cenário 1: chamada permitida, ainda longe do limite
Nem toda API precisa retornar headers de rate limit em toda resposta. Em endpoints muito chamados, isso pode ser desnecessário. Mas em APIs públicas, integrações críticas ou clientes que precisam se autorregular, retornar os headers mesmo em respostas bem-sucedidas ajuda bastante. O cliente consegue reduzir a taxa antes de bater no limite, em vez de descobrir o contrato apenas quando recebe 429.
Um exemplo de resposta saudável seria:
HTTP/1.1 200 OK
Content-Type: application/json
RateLimit-Policy: "orders-read";q=300;w=60
RateLimit: "orders-read";r=247;t=42
{
"id": "ORD-123",
"status": "paid"
}
Nesse exemplo, a política orders-read permite 300 unidades de quota em uma janela de 60 segundos. O header RateLimit informa que ainda restam 247 unidades e que a janela efetiva reseta em 42 segundos. O cliente não precisa conhecer detalhes internos do algoritmo. Ele só precisa entender que existe uma política, que ainda há capacidade disponível e que chamadas futuras devem respeitar essa janela.
Cenário 2: chamada permitida, mas perto do limite
Quando o cliente está perto de consumir a quota, a API pode continuar respondendo 200 OK, mas os headers começam a sinalizar que ele deveria reduzir o ritmo. Esse é um cenário importante porque evita que o cliente só reaja depois da falha. Um bom consumidor pode usar esses sinais para aplicar backoff preventivo, diminuir concorrência ou pausar jobs de baixa prioridade.
HTTP/1.1 200 OK
Content-Type: application/json
RateLimit-Policy: "orders-read";q=300;w=60
RateLimit: "orders-read";r=8;t=19
{
"id": "ORD-123",
"status": "paid"
}
Aqui a chamada ainda foi aceita, mas restam apenas 8 unidades até a janela resetar em 19 segundos. Um cliente bem implementado não precisa esperar o 429. Ele pode desacelerar sozinho. Essa é a diferença entre rate limiting como bloqueio e rate limiting como contrato: a API não está apenas punindo excesso; ela está dando ao consumidor informação suficiente para evitar o excesso.
Cenário 3: limite de janela excedido
Quando o cliente esgota a janela, a resposta deve ser explícita. O status correto é 429 Too Many Requests, e Retry-After deve indicar em quantos segundos o cliente pode tentar novamente. Também é útil manter os headers de rate limit para que o consumidor entenda a política violada.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 19
RateLimit-Policy: "orders-read";q=300;w=60
RateLimit: "orders-read";r=0;t=19
{
"type": "https://api.example.com/problems/rate-limit-exceeded",
"title": "Rate limit exceeded",
"detail": "The orders-read limit was exceeded. Try again in 19 seconds.",
"policy": "orders-read"
}
Essa resposta tem três qualidades importantes. Primeiro, usa o status correto. Segundo, informa quando tentar novamente. Terceiro, explica qual política foi violada. O corpo em application/problem+json não é obrigatório, mas ajuda muito em APIs consumidas por times diferentes, parceiros ou clientes externos, porque deixa o erro legível e padronizado.
Cenário 4: quota diária ou mensal esgotada
Nem todo limite é uma janela curta. Algumas APIs também têm quotas diárias, mensais ou por plano. Nesse caso, o cliente talvez não deva tentar novamente em poucos segundos. Ele precisa saber que a limitação é de outro tipo. Se a quota volta no próximo dia, 429 ainda pode fazer sentido, com Retry-After apontando para o tempo até a renovação. Se a limitação depende de upgrade comercial ou permissão, talvez 403 Forbidden seja mais adequado. A diferença é importante: 429 comunica “você pode tentar depois”; 403 comunica “você não tem permissão para isso nas condições atuais”.
Um exemplo de quota diária temporariamente esgotada:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 18300
RateLimit-Policy: "reports-daily";q=1000;w=86400
RateLimit: "reports-daily";r=0;t=18300
{
"type": "https://api.example.com/problems/quota-exceeded",
"title": "Daily quota exceeded",
"detail": "The daily quota for report generation was exhausted. Try again after the quota resets.",
"policy": "reports-daily"
}
A boa prática aqui é deixar claro que o problema não é uma rajada curta. O cliente não deveria fazer retry a cada segundo esperando sucesso. Ele deveria reagendar o job, avisar o usuário ou reduzir o consumo até a próxima janela. Se a API não diferencia rate limit curto de quota diária, o consumidor tende a tratar tudo como retry rápido, o que piora a experiência e aumenta ruído operacional.
Cenário 5: servidor temporariamente degradado
Existe uma diferença entre “este cliente excedeu sua quota” e “o serviço está temporariamente sem capacidade”. Se a restrição é causada pelo comportamento do consumidor, 429 é o caminho natural. Mas se o serviço está degradado, em manutenção, saturado ou protegendo uma dependência crítica, 503 Service Unavailable pode ser mais honesto. Nesse caso, Retry-After continua sendo útil, mas o significado muda: o problema não é apenas aquele cliente; é uma redução temporária de capacidade do servidor.
HTTP/1.1 503 Service Unavailable
Content-Type: application/problem+json
Retry-After: 30
RateLimit-Policy: "payments-authorize";q=100;w=60
RateLimit: "payments-authorize";r=0;t=30
{
"type": "https://api.example.com/problems/temporary-capacity-reduction",
"title": "Temporary capacity reduction",
"detail": "Payment authorization is temporarily limited due to provider degradation. Retry after 30 seconds.",
"policy": "payments-authorize"
}
Esse cenário é comum quando uma dependência externa degrada. Imagine um provider de pagamento lento. Se a API continuar aceitando tudo, a degradação pode se espalhar para checkout, pedidos, filas e atendimento. Reduzir temporariamente a capacidade e comunicar isso com headers claros é melhor do que deixar o sistema falhar de forma opaca.
Cenário 6: limite de concorrência atingido
Rate limiting não é apenas “requisições por minuto”. Algumas operações precisam de limite de concorrência, porque o gargalo real é o número de operações simultâneas. Exportar relatórios, chamar um provider externo, processar imagens, consultar um modelo de IA ou executar uma query pesada pode exigir esse tipo de controle.
Se o limite de concorrência é por consumidor, 429 costuma ser adequado. O cliente está acima da capacidade combinada para aquele tipo de operação.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 5
RateLimit-Policy: "reports-concurrency";q=3;w=1
RateLimit: "reports-concurrency";r=0;t=5
{
"type": "https://api.example.com/problems/concurrency-limit-exceeded",
"title": "Concurrency limit exceeded",
"detail": "Only 3 concurrent report exports are allowed per tenant.",
"policy": "reports-concurrency"
}
Aqui o importante é explicar que o limite é de concorrência, não de volume por minuto. O cliente não deveria simplesmente aumentar retries. Ele deveria aguardar uma exportação terminar antes de iniciar outra, ou usar uma fila própria. Quando a API comunica isso bem, o consumidor consegue ajustar arquitetura, não apenas tratar erro.
Como implementar o básico em ASP.NET Core
Em ASP.NET Core, o rate limiter pode ser configurado por política. O exemplo abaixo mostra uma ideia simples: limite por tenant para leitura de pedidos. Em produção, o tenant deve vir de autenticação ou claims confiáveis, não de um header arbitrário.
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("orders-read", httpContext =>
{
var tenantId = httpContext.User.FindFirst("tenant_id")?.Value
?? "anonymous";
return RateLimitPartition.GetFixedWindowLimiter(
partitionKey: tenantId,
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 300,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
});
});
options.OnRejected = async (context, token) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers.RetryAfter = "60";
context.HttpContext.Response.ContentType = "application/problem+json";
await context.HttpContext.Response.WriteAsJsonAsync(new
{
type = "https://api.example.com/problems/rate-limit-exceeded",
title = "Rate limit exceeded",
detail = "Too many requests. Try again later."
}, cancellationToken: token);
};
});
app.UseRateLimiter();
app.MapGet("/orders/{id}", GetOrder)
.RequireRateLimiting("orders-read");
Esse exemplo não resolve todos os detalhes de headers dinâmicos, janela restante e múltiplas políticas, mas mostra a direção correta: o limite é por consumidor, a rota declara a política e a rejeição retorna 429 com corpo padronizado. Em um sistema mais maduro, eu adicionaria headers com a política aplicada, quota restante, tempo até reset e um Retry-After calculado a partir da janela real.
Boas práticas para cabeçalhos de rate limit
Uma API bem desenhada deveria ser previsível para clientes bem comportados. Isso significa que os headers precisam orientar ação. Em respostas bem-sucedidas, RateLimit-Policy e RateLimit ajudam o consumidor a entender a política e quanto ainda resta. Nas respostas rejeitadas por excesso, Retry-After deve dizer quando tentar novamente. Para quotas longas, a resposta deve deixar claro que o cliente não deve insistir em retries curtos. Em degradação temporária do servidor, 503 com Retry-After pode comunicar melhor que a capacidade do serviço foi reduzida.
Algumas APIs ainda usam headers legados como X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset. Eles são comuns e podem ser mantidos por compatibilidade, mas eu evitaria criar uma API nova baseada apenas nesses nomes se houver espaço para adotar uma representação mais alinhada aos headers modernos. O ponto não é discutir nomenclatura por purismo. O ponto é garantir que o cliente receba três informações: qual política está em vigor, quanto ainda pode consumir e quando deve tentar novamente se exceder.
Também vale evitar excesso de detalhe. Headers de rate limit não deveriam expor informações sensíveis sobre capacidade interna. Em APIs públicas, revelar limites muito específicos pode ajudar atacantes a calibrar abuso. O ideal é comunicar o suficiente para clientes legítimos se comportarem bem, sem transformar headers em documentação completa da infraestrutura.
Fechando a ideia
Rate limiting bem feito não é apenas uma trava. É uma conversa entre provedor e consumidor da API. A API diz qual capacidade está disponível, como essa capacidade é medida, o que acontece quando ela é excedida e quando o cliente pode tentar novamente. O consumidor, por sua vez, consegue aplicar backoff, reduzir concorrência, reagendar jobs e evitar retries que pioram a situação.
A maturidade está em sair do limite genérico e pensar por cenário. Chamada permitida pode retornar headers para autorregulação. Chamada perto do limite pode incentivar desaceleração. Excesso de janela curta deve retornar 429 com Retry-After. Quota diária precisa indicar que insistir agora não ajuda. Degradação temporária do serviço pode ser melhor representada por 503. Limite de concorrência precisa dizer que o problema não é apenas volume, mas operações simultâneas.
No fim, rate limiting não pergunta apenas “quantas chamadas devo permitir?”. Ele pergunta qual capacidade a API está prometendo, para quem, em qual janela e com qual comportamento quando esse contrato for excedido. Essa é a diferença entre bloquear tráfego e desenhar uma API que sabe operar seus próprios limites. Vale também conferir Estratégias de Resiliência para Microservices: Aplicando Back-pressure e Bulkhead com Eficiência
