.NET ASP.NET Core Segurança Performance Web API DevOps

Rate limiting nativo no .NET 8/9: proteja suas APIs sem dependências externas

Guia completo do rate limiting nativo do ASP.NET Core: fixed window, sliding window, token bucket, concurrency limiter.

N
Neryx Digital Architects
27 de janeiro de 2026
12 min de leitura
220 profissionais leram
Categoria: Arquitetura Público: Times de plataforma e operação Etapa: Decisão

Antes do .NET 7, implementar rate limiting exigia pacotes externos como AspNetCoreRateLimit. A partir do .NET 7 (consolidado no .NET 8/9), o ASP.NET Core tem suporte nativo via Microsoft.AspNetCore.RateLimiting, com quatro algoritmos prontos, políticas nomeadas e integração direta com o pipeline de autenticação. Este artigo cobre tudo que você precisa para proteger suas APIs em produção.

Os quatro algoritmos disponíveis

Cada algoritmo resolve um problema diferente:

  • Fixed Window — X requisições por janela fixa de tempo (ex: 100/minuto). Simples, mas sofre de "burst" no limite da janela.
  • Sliding Window — Janela deslizante que elimina o burst. Mais preciso, levemente mais custoso.
  • Token Bucket — Tokens acumulados até um máximo, consumidos por requisição. Permite bursts controlados (tokens acumulados).
  • Concurrency Limiter — Limita requisições simultâneas, não por tempo. Ideal para proteger recursos com limite de conexões.

Setup e configuração

// Program.cs
using System.Threading.RateLimiting;

builder.Services.AddRateLimiter(options =>
{
    // Comportamento quando o limite é atingido
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Handler customizado para resposta de rejeição
    options.OnRejected = async (context, cancellationToken) =>
    {
        context.HttpContext.Response.StatusCode = 429;

        // Adiciona header Retry-After se disponível
        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",
            Message = "Limite de requisições atingido. Tente novamente em instantes.",
            RetryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var ra)
                ? (int?)ra.TotalSeconds
                : null
        }, cancellationToken);
    };

    // ── Política 1: Fixed Window global ──────────────────────────────────
    options.AddFixedWindowLimiter("global", opt =>
    {
        opt.PermitLimit = 1000;               // 1000 requisições
        opt.Window = TimeSpan.FromMinutes(1); // por minuto
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 0;                   // Sem fila — rejeita imediatamente
    });

    // ── Política 2: Sliding Window por IP ────────────────────────────────
    options.AddSlidingWindowLimiter("per-ip", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.SegmentsPerWindow = 6; // Divide em segmentos de 10s
        opt.QueueLimit = 0;
    });

    // ── Política 3: Token Bucket para endpoints custosos ─────────────────
    options.AddTokenBucketLimiter("expensive-operations", opt =>
    {
        opt.TokenLimit = 10;                          // Máximo de 10 tokens
        opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10); // Reabastece a cada 10s
        opt.TokensPerPeriod = 2;                      // 2 tokens por período
        opt.AutoReplenishment = true;
        opt.QueueLimit = 5;                           // Fila de até 5 requisições
    });

    // ── Política 4: Concurrency Limiter para DB ───────────────────────────
    options.AddConcurrencyLimiter("database-intensive", opt =>
    {
        opt.PermitLimit = 20; // Máximo de 20 queries simultâneas
        opt.QueueProcessingOrder = QueueProcessingOrder.NewestFirst;
        opt.QueueLimit = 10;
    });

    // ── Política 5: Por usuário autenticado (a mais importante) ───────────
    options.AddPolicy("per-user", context =>
    {
        // Usuário autenticado: limite generoso
        if (context.User.Identity?.IsAuthenticated == true)
        {
            var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                ?? context.User.FindFirst(ClaimTypes.Email)?.Value
                ?? "authenticated";

            return RateLimitPartition.GetSlidingWindowLimiter(
                partitionKey: $"user:{userId}",
                factory: _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = 500,
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 6,
                    QueueLimit = 0
                });
        }

        // Usuário anônimo: limite por IP (mais restritivo)
        var ipAddress = context.Connection.RemoteIpAddress?.ToString()
            ?? context.Request.Headers["X-Forwarded-For"].FirstOrDefault()
            ?? "unknown";

        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: $"ip:{ipAddress}",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 30,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0
            });
    });

    // ── Política 6: Por tenant (multi-tenancy) ────────────────────────────
    options.AddPolicy("per-tenant", context =>
    {
        var tenantId = context.Items["TenantId"]?.ToString()
            ?? context.Request.Headers["X-Tenant-Id"].FirstOrDefault()
            ?? "unknown";

        // Limite configurável por tenant (ex: plano Basic vs Premium)
        var isPremium = context.User.HasClaim("plan", "premium");

        return RateLimitPartition.GetTokenBucketLimiter(
            partitionKey: $"tenant:{tenantId}",
            factory: _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = isPremium ? 1000 : 100,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                TokensPerPeriod = isPremium ? 1000 : 100,
                AutoReplenishment = true
            });
    });
});

// Aplicar o middleware
app.UseRateLimiter();

Aplicando políticas nos endpoints

// Minimal APIs — política por endpoint
app.MapGet("/api/products", GetAllProducts)
    .RequireRateLimiting("per-user");

app.MapPost("/api/reports/generate", GenerateReport)
    .RequireRateLimiting("expensive-operations"); // Token bucket para operações pesadas

app.MapPost("/api/auth/login", Login)
    .RequireRateLimiting("per-ip"); // IP-based para endpoints de auth (anti-brute-force)

// Desabilitar rate limiting para endpoints internos
app.MapGet("/health", () => Results.Ok())
    .DisableRateLimiting();

// Controllers — atributo
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("per-user")] // Aplica à classe toda
public class OrdersController : ControllerBase
{
    [HttpPost]
    [EnableRateLimiting("per-tenant")] // Sobrescreve para este endpoint
    public async Task<IActionResult> Create([FromBody] CreateOrderRequest request)
    {
        // ...
    }

    [HttpGet("export")]
    [EnableRateLimiting("expensive-operations")]
    public async Task<IActionResult> Export()
    {
        // ...
    }

    [HttpGet("/health/orders")]
    [DisableRateLimiting] // Sem limite para health check
    public IActionResult Health() => Ok();
}

Rate limiting para múltiplas instâncias com Redis

O rate limiting nativo usa contadores em memória — cada instância/pod tem seus próprios contadores. Em Kubernetes com 3 réplicas, o usuário pode fazer 3x o limite permitido. Para distribuir os contadores, use Redis:

dotnet add package RedisRateLimiting
// Rate limiting distribuído com Redis
builder.Services.AddStackExchangeRedisCache(options =>
    options.Configuration = builder.Configuration.GetConnectionString("Redis"));

var redisConnection = ConnectionMultiplexer.Connect(
    builder.Configuration.GetConnectionString("Redis")!);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisConnection);

builder.Services.AddRateLimiter(options =>
{
    // Sliding window distribuído via Redis
    options.AddPolicy("per-user-distributed", context =>
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "anonymous";

        return RedisRateLimitPartition.GetSlidingWindowRateLimiter(
            partitionKey: $"ratelimit:user:{userId}",
            factory: key => new RedisSlidingWindowRateLimiterOptions
            {
                ConnectionMultiplexerFactory = () =>
                    context.RequestServices.GetRequiredService<IConnectionMultiplexer>(),
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                SegmentsPerWindow = 6
            });
    });
});

Adicionando headers informativos à resposta

// Middleware que adiciona headers X-RateLimit-* a todas as respostas
public class RateLimitHeadersMiddleware
{
    private readonly RequestDelegate _next;

    public RateLimitHeadersMiddleware(RequestDelegate next)
        => _next = next;

    public async Task InvokeAsync(HttpContext context)
    {
        await _next(context);

        // Adiciona headers informativos se disponíveis
        var rateLimitLease = context.Features.Get<IRateLimiterStatisticsFeature>();
        if (rateLimitLease is not null)
        {
            if (rateLimitLease.CurrentAvailablePermits.HasValue)
                context.Response.Headers["X-RateLimit-Remaining"] =
                    rateLimitLease.CurrentAvailablePermits.Value.ToString();

            if (rateLimitLease.CurrentWindowDuration.HasValue)
                context.Response.Headers["X-RateLimit-Reset"] =
                    ((int)rateLimitLease.CurrentWindowDuration.Value.TotalSeconds).ToString();
        }
    }
}

// Registrar antes do UseRateLimiter
app.UseMiddleware<RateLimitHeadersMiddleware>();
app.UseRateLimiter();

Testes de rate limiting

// Teste de integração — verifica que o 429 é retornado após o limite
public class RateLimitingTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public RateLimitingTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Limite de 3 requisições para facilitar o teste
                services.PostConfigure<RateLimiterOptions>(options =>
                {
                    options.AddFixedWindowLimiter("per-ip", opt =>
                    {
                        opt.PermitLimit = 3;
                        opt.Window = TimeSpan.FromSeconds(60);
                    });
                });
            });
        }).CreateClient();
    }

    [Fact]
    public async Task RateLimit_ExceedsLimit_Returns429()
    {
        // Primeiras 3 requisições devem passar
        for (int i = 0; i < 3; i++)
        {
            var response = await _client.GetAsync("/api/products");
            response.StatusCode.Should().Be(HttpStatusCode.OK);
        }

        // 4ª deve retornar 429
        var limitedResponse = await _client.GetAsync("/api/products");
        limitedResponse.StatusCode.Should().Be(HttpStatusCode.TooManyRequests);
        limitedResponse.Headers.Should().ContainKey("Retry-After");
    }
}

Escolhendo o algoritmo certo

Caso de uso Algoritmo recomendado Por quê
Proteção geral de API Sliding Window Sem burst no limite da janela
Anti brute-force (login) Fixed Window por IP Simples e eficiente
Operações custosas (relatórios) Token Bucket Permite acúmulo para bursts ocasionais
Proteção de banco/recursos Concurrency Limiter Limita concurrent, não por tempo
Múltiplos pods/Kubernetes Sliding Window + Redis Contadores distribuídos
Planos diferenciados (SaaS) Token Bucket por tenant Configurável por plano de assinatura

Rate limiting é a primeira linha de defesa contra abuso de APIs. Se você precisa de uma estratégia de segurança para sua API .NET, fale com a Neryx.

Quer sair do modo reativo e priorizar o que mais importa?

O diagnóstico de maturidade ajuda a transformar sintomas operacionais em um plano mais claro de evolução.

Avaliar maturidade

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.