Multi‑Tenant Rate Limiting no .NET 10 e C# 14

O rate limiting por tenant é a forma de garantir que cada cliente de uma aplicação SaaS tenha sua própria cota de requisições, sem que um interfira no outro. Com o .NET 10 e o C# 14, esse controle fica direto de implementar usando apenas o middleware nativo do ASP.NET Core.

Esse recurso é particularmente relevante quando múltiplos clientes compartilham a mesma infraestrutura e exigem políticas de uso diferenciadas. Neste artigo, você monta uma implementação completa, do modelo de dados até a resposta 429.

O que é rate limiting por tenant

O rate limiting é um mecanismo que restringe o número de requisições que um cliente pode fazer em um intervalo de tempo. Quando a cota acaba, a API recusa novas chamadas até o limite ser reposto.

Já o rate limiting por tenant aplica essa regra separadamente para cada cliente da plataforma. Em outras palavras, cada tenant ganha seu próprio contador, e o consumo de um não reduz a cota do outro.

Por exemplo, um cliente no plano gratuito pode ter 60 requisições por minuto, enquanto um cliente corporativo tem 600. Ambos usam a mesma API, mas com regras distintas.

Vale diferenciar esse controle do limite global da aplicação. O limite global protege o servidor como um todo, enquanto o rate limiting por tenant distribui a capacidade de forma justa entre quem paga por ela.

Na prática, as duas abordagens podem conviver. Por isso, muitas APIs combinam um teto geral de segurança com cotas individuais definidas pelo contrato de cada cliente.

Por que isolar limites em cenários multi-tenant

Em cenários multi-tenant, esse controle é essencial para garantir quatro coisas:

  • Uso justo dos recursos compartilhados.
  • Proteção da aplicação contra abusos ou picos inesperados de tráfego.
  • Qualidade de serviço consistente entre diferentes clientes.
  • Diferenciação de planos comerciais.

Sem isolamento, surge o problema do noisy neighbor, ou vizinho barulhento. Nesse caso, um único cliente com tráfego excessivo consome a capacidade da aplicação e degrada a experiência de todos os outros.

Além disso, limites por cliente ajudam no controle de custos. Afinal, em serviços cobrados por uso, saber quanto cada tenant pode consumir facilita prever a infraestrutura necessária.

O que o .NET 10 oferece para controlar tráfego

O suporte a rate limiting no ASP.NET Core vem do middleware Microsoft.AspNetCore.RateLimiting, construído sobre a biblioteca System.Threading.RateLimiting. Vale um esclarecimento: esse middleware e as partições já existem desde o .NET 7, e o .NET 10 continua usando a mesma base.

Portanto, o ganho no .NET 10 não está em uma API nova de multi-tenant. Na prática, ele vem da maturidade do middleware, das métricas embutidas e dos recursos do C# 14, que deixam o código mais enxuto.

O middleware oferece quatro algoritmos, segundo a documentação oficial de rate limiting no ASP.NET Core:

  • Fixed window, que usa uma janela de tempo fixa.
  • Sliding window, que divide a janela em segmentos.
  • Token bucket, que repõe uma quantidade fixa de tokens por período.
  • Concurrency, que limita apenas as requisições simultâneas.

O ponto central para o cenário multi-tenant são as partições. Elas dividem o tráfego em grupos separados, cada um com seus próprios contadores, e a chave da partição pode ser o identificador do cliente.

Se você quer revisar os fundamentos antes, o artigo sobre rate limiting no ASP.NET Core 10 cobre os algoritmos com mais detalhes.

Os três pilares do rate limiting por tenant

A implementação de rate limiting por tenant segue três pilares principais:

  1. Identificação do tenant a partir da requisição HTTP.
  2. Definição dinâmica dos limites de requisições por tenant.
  3. Aplicação das políticas usando particionamento.

No rate limiting por tenant, cada pilar fica em um componente próprio. Dessa forma, você troca a origem dos limites ou a forma de identificar o cliente sem mexer na configuração do middleware.

Modelo de dados para limites por tenant

A definição de limites pode ser um modelo simples, que associa o identificador do tenant à sua cota de requisições. Aqui, o C# 14 já ajuda com a palavra-chave field, que dá acesso ao campo de apoio gerado pelo compilador.

public class TenantRateLimit
{
    public string TenantId { get; set; } = string.Empty;
    public TenantPlan Plan { get; set; } = TenantPlan.Free;

    // C# 14: valida o valor sem declarar um campo privado
    public int RequestsPerMinute
    {
        get;
        set => field = value > 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "The limit must be greater than zero.");
    }
}

Antes do C# 14, essa validação exigia declarar um campo privado e implementar os dois acessores. Agora, apenas o set ganha corpo, e o get continua automático.

Em seguida, um serviço devolve o limite de cada tenant. Neste exemplo, os dados ficam em memória, mas a mesma interface pode ler de um banco ou de um serviço de configuração:

public interface ITenantLimitStore
{
    TenantRateLimit GetLimit(string tenantId);
}

public sealed class InMemoryTenantLimitStore : ITenantLimitStore
{
    // Limite aplicado a tenants desconhecidos
    private static readonly TenantRateLimit DefaultLimit =
        new() { TenantId = "default", RequestsPerMinute = 60 };

    private readonly Dictionary<string, TenantRateLimit> _limits = new()
    {
        ["acme"] = new() { TenantId = "acme", RequestsPerMinute = 600 },
        ["contoso"] = new() { TenantId = "contoso", RequestsPerMinute = 120 }
    };

    public TenantRateLimit GetLimit(string tenantId)
        => _limits.GetValueOrDefault(tenantId, DefaultLimit);
}

Repare que tenants desconhecidos caem no limite padrão. Esse detalhe é importante para a segurança, como você vai ver mais adiante.

Resolução do tenant

Para que o rate limiting por tenant funcione de forma isolada, a aplicação precisa identificar o cliente em cada requisição. No exemplo original, um serviço extrai o identificador de um cabeçalho HTTP personalizado chamado X-Tenant-ID.

public sealed class TenantResolver
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantResolver(IHttpContextAccessor httpContextAccessor)
        => _httpContextAccessor = httpContextAccessor;

    public string ResolveTenant()
    {
        var tenantId = _httpContextAccessor.HttpContext?
            .Request.Headers["X-Tenant-ID"].ToString();

        // ToString de um cabeçalho ausente retorna string vazia, não null
        return string.IsNullOrWhiteSpace(tenantId) ? "default" : tenantId;
    }
}

Note a correção no retorno. Um cabeçalho ausente gera uma string vazia, então o operador ?? sozinho nunca aplicaria o valor padrão.

Para usar essa classe, registre o acessor de contexto e o próprio resolvedor no container de injeção de dependência:

// Permite acessar o HttpContext fora dos controllers
builder.Services.AddHttpContextAccessor();

// Uma instância por requisição
builder.Services.AddScoped<TenantResolver>();

Esse padrão desacopla a identificação do tenant da configuração do rate limiting, o que favorece a manutenção. Contudo, o C# 14 permite uma alternativa ainda mais direta com os extension members.

public static class HttpContextTenantExtensions
{
    // C# 14: bloco de extensão com uma propriedade
    extension(HttpContext context)
    {
        public string TenantId
        {
            get
            {
                // Prefere a claim do token autenticado
                var claim = context.User.FindFirst("tenant_id")?.Value;
                if (!string.IsNullOrWhiteSpace(claim))
                    return claim;

                var header = context.Request.Headers["X-Tenant-ID"].ToString();
                return string.IsNullOrWhiteSpace(header) ? "default" : header;
            }
        }
    }
}

Com isso, qualquer código que tenha um HttpContext lê httpContext.TenantId como se fosse uma propriedade nativa. O artigo sobre extension members no C# 14 explica essa sintaxe em detalhes.

Repare também na ordem de leitura. A claim vem de um token validado, enquanto o cabeçalho pode ser forjado por qualquer cliente.

Configuração do rate limiting por tenant com token bucket

A configuração acontece na inicialização da aplicação, com o método AddRateLimiter. Nesse ponto, você define uma política que cria limitadores particionados por tenant usando o algoritmo token bucket.

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

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<ITenantLimitStore, InMemoryTenantLimitStore>();

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("TenantPolicy", httpContext =>
    {
        var store = httpContext.RequestServices.GetRequiredService<ITenantLimitStore>();
        var limit = store.GetLimit(httpContext.TenantId);

        // A chave usa o tenant conhecido e o limite, nunca o valor bruto do cabeçalho
        var partitionKey = $"{limit.TenantId}:{limit.RequestsPerMinute}";

        return RateLimitPartition.GetTokenBucketLimiter(
            partitionKey,
            _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = limit.RequestsPerMinute,
                TokensPerPeriod = limit.RequestsPerMinute,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                QueueLimit = 0,
                AutoReplenishment = true
            });
    });
});

No rate limiting por tenant, cada cliente recebe sua própria partição de tokens, o que garante isolamento. Assim, o tráfego intenso da acme não afeta a cota da contoso.

Se todas as rotas da API devem respeitar a cota do cliente, existe a alternativa do limitador global. Nesse caso, o rate limiting por tenant vale para qualquer endpoint, sem precisar de RequireRateLimiting:

builder.Services.AddRateLimiter(options =>
{
    // Aplica o limite por tenant a todos os endpoints
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
    {
        var store = httpContext.RequestServices.GetRequiredService<ITenantLimitStore>();
        var limit = store.GetLimit(httpContext.TenantId);

        return RateLimitPartition.GetFixedWindowLimiter(
            $"{limit.TenantId}:{limit.RequestsPerMinute}",
            _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = limit.RequestsPerMinute,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0
            });
    });
});

A política nomeada dá mais controle, porque você escolhe onde aplicar. Por outro lado, o limitador global evita esquecer alguma rota nova sem proteção.

Existe um detalhe sutil na chave da partição. O framework cria e guarda em cache um limitador para cada chave distinta, então incluir o limite na chave faz uma mudança de plano gerar uma partição nova com a cota atualizada.

Como o token bucket funciona

O token bucket parece um balde com capacidade máxima de tokens. Cada requisição consome um token, e a cada período o balde recebe uma quantidade fixa de tokens novos, sem passar da capacidade.

Na configuração acima, o TokenLimit define a capacidade, o TokensPerPeriod define quantos tokens entram por período e o ReplenishmentPeriod define o período. Com o QueueLimit igual a zero, a requisição sem token é recusada na hora, sem fila.

Esse algoritmo combina com o rate limiting por tenant porque tolera rajadas curtas. Por exemplo, um cliente pode gastar toda a cota em poucos segundos, desde que respeite a média ao longo do tempo.

Respostas 429 e Retry-After

Quando o limite estoura, a API deve responder com o status 429 Too Many Requests. Além disso, informar o cabeçalho Retry-After ajuda o cliente a saber quando tentar de novo.

builder.Services.AddRateLimiter(options =>
{
    options.OnRejected = async (context, cancellationToken) =>
    {
        // Informa quando o cliente pode tentar novamente, se o limitador souber
        if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
        }

        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        await context.HttpContext.Response.WriteAsync("Too many requests.", cancellationToken);
    };
});

Se a sua API já padroniza erros, vale devolver um ProblemDetails no lugar do texto simples. Dessa forma, o cliente recebe o erro no mesmo formato das outras falhas.

Aplicação do rate limiting por tenant nos endpoints

Depois de definir a política, ative o middleware no pipeline HTTP com UseRateLimiter. Em seguida, aplique a política de forma seletiva aos endpoints.

var app = builder.Build();

app.UseRouting();

// Deve vir depois do UseRouting quando há políticas por endpoint
app.UseRateLimiter();

app.MapGet("/api/orders", () => Results.Ok(Array.Empty<string>()))
   .RequireRateLimiting("TenantPolicy");

// Health check sem limite
app.MapGet("/health", () => Results.Ok())
   .DisableRateLimiting();

app.Run();

Em controllers, use o atributo [EnableRateLimiting] na classe ou na action. Já o [DisableRateLimiting] desliga o limite para um ponto específico, mesmo quando há política aplicada de forma mais ampla.

[ApiController]
[Route("api/invoices")]
[EnableRateLimiting("TenantPolicy")]
public class InvoicesController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(Array.Empty<string>());

    // Endpoint interno sem limite de requisições
    [HttpGet("status")]
    [DisableRateLimiting]
    public IActionResult GetStatus() => Ok();
}

Essa abordagem controla exatamente quais recursos ficam sujeitos ao rate limiting por tenant. Segundo a documentação, o UseRateLimiter precisa vir depois do UseRouting quando você usa APIs de rate limiting por endpoint.

Cuidados de segurança no rate limiting por tenant

No rate limiting por tenant, o primeiro cuidado é com a chave da partição. A documentação alerta que criar partições a partir de dados controlados pelo usuário deixa a aplicação vulnerável a ataques de negação de serviço.

Isso acontece porque cada chave distinta cria e guarda um limitador em memória. Portanto, um atacante que envia milhares de valores diferentes no X-Tenant-ID poderia esgotar a memória, e por isso o exemplo mapeia tenants desconhecidos para a partição padrão.

O segundo cuidado é com a origem do tenant. Sempre que possível, leia o identificador de uma claim do token autenticado, já que o cabeçalho pode ser alterado livremente.

O terceiro ponto envolve múltiplas instâncias. O middleware guarda os contadores na memória de cada processo, então três réplicas da API significam, na prática, até três vezes a cota por tenant.

Por fim, monitore e teste. O middleware publica métricas no medidor Microsoft.AspNetCore.RateLimiting, e a recomendação oficial é fazer testes de carga antes de ir para produção.

Limites diferentes por plano no rate limiting por tenant

Em muitos produtos SaaS, o plano define não só a cota, mas também o comportamento do limite. Por exemplo, o plano gratuito pode usar uma janela deslizante mais rígida, enquanto os planos pagos usam token bucket para tolerar rajadas.

Para isso, basta acrescentar o plano ao modelo e escolher o algoritmo com uma expressão switch:

public enum TenantPlan
{
    Free,
    Pro,
    Enterprise
}

builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("TenantPlanPolicy", httpContext =>
    {
        var store = httpContext.RequestServices.GetRequiredService<ITenantLimitStore>();
        var limit = store.GetLimit(httpContext.TenantId);
        var partitionKey = $"{limit.TenantId}:{limit.Plan}:{limit.RequestsPerMinute}";

        // Cada plano usa o algoritmo mais adequado ao seu perfil de uso
        return limit.Plan switch
        {
            TenantPlan.Free => RateLimitPartition.GetSlidingWindowLimiter(
                partitionKey,
                _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = limit.RequestsPerMinute,
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 6,
                    QueueLimit = 0
                }),

            _ => RateLimitPartition.GetTokenBucketLimiter(
                partitionKey,
                _ => new TokenBucketRateLimiterOptions
                {
                    TokenLimit = limit.RequestsPerMinute,
                    TokensPerPeriod = limit.RequestsPerMinute,
                    ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                    QueueLimit = 0
                })
        };
    });
});

Nesse exemplo, o modelo TenantRateLimit usa a propriedade Plan, do tipo TenantPlan. Com isso, o rate limiting por tenant passa a refletir diretamente a tabela de preços do produto.

Na janela deslizante, o minuto é dividido em seis segmentos de dez segundos. Assim, o limite não zera de uma vez no fim da janela, o que evita picos logo após a virada do minuto.

Como testar o rate limiting por tenant

Um teste rápido ajuda a confirmar o isolamento entre clientes. O script abaixo dispara requisições para dois tenants e mostra apenas o status HTTP de cada resposta:

# Envia 130 requisições como contoso (limite de 120 por minuto)
for i in $(seq 1 130); do
  curl -s -o /dev/null -w "%{http_code}\n" -H "X-Tenant-ID: contoso" https://localhost:5001/api/orders
done | sort | uniq -c

# Em seguida, confirma que a acme continua sendo atendida
curl -s -o /dev/null -w "%{http_code}\n" -H "X-Tenant-ID: acme" https://localhost:5001/api/orders

O resultado esperado são 120 respostas 200 e 10 respostas 429 para a contoso. Já a chamada da acme deve retornar 200, provando que o rate limiting por tenant isolou os contadores.

Para acompanhar em tempo real, monitore o medidor do middleware com a ferramenta dotnet-counters:

# Acompanha as métricas de rate limiting do processo
dotnet-counters monitor --name OrdersApi --counters Microsoft.AspNetCore.RateLimiting

A métrica aspnetcore.rate_limiting.requests conta as tentativas de obter permissão, com o resultado em uma tag. Além disso, a tag aspnetcore.rate_limiting.policy mostra qual política tratou cada requisição.

Benefícios do rate limiting por tenant no .NET 10

A implementação de rate limiting por tenant no .NET 10 traz uma série de vantagens técnicas:

  • Integração nativa com o ASP.NET Core, sem dependências externas.
  • Isolamento por tenant, evitando o efeito noisy neighbor.
  • Configuração centralizada, facilitando a manutenção e a evolução.
  • Flexibilidade para planos diferenciados, com limites definidos por cliente.
  • Código mais enxuto com o field e os extension members do C# 14.

Além disso, o mesmo desenho aceita outros algoritmos. Por exemplo, você pode trocar o token bucket por sliding window em um plano específico apenas mudando o método RateLimitPartition usado.

Conclusão

O rate limiting por tenant no .NET 10 resolve um problema clássico de aplicações SaaS: impedir que um cliente afete os demais. Com partições, cada tenant ganha seu próprio balde de tokens e regras alinhadas ao seu plano.

O middleware nativo existe desde o .NET 7, mas o .NET 10 e o C# 14 deixam a implementação mais limpa. O field simplifica a validação do modelo, e os extension members tornam a leitura do tenant natural.

Ainda assim, alguns cuidados fazem diferença. Use chaves de partição controladas, prefira claims autenticadas e lembre que os contadores ficam na memória de cada instância.

Em resumo, com o rate limiting por tenant a plataforma reduz a complexidade arquitetural e melhora a governança de recursos. Isso reforça o .NET como uma boa escolha para aplicações escaláveis e prontas para alta concorrência.

FAQ

O .NET 10 criou uma API específica para multi-tenant?

Não. O rate limiting por tenant usa as partições do middleware que existe desde o .NET 7, combinadas com recursos do C# 14.

Qual algoritmo usar para limitar requisições por cliente?

O token bucket é uma boa escolha, porque tolera rajadas curtas e mantém a média ao longo do tempo. Para limitar chamadas simultâneas, use o concurrency limiter.

Posso usar um cabeçalho HTTP para identificar o tenant?

Pode, mas o cabeçalho é controlado pelo cliente. Prefira uma claim de um token validado e mapeie valores desconhecidos para uma partição padrão.

O limite vale para todas as instâncias da API?

Não. Os contadores ficam na memória de cada processo, então cada réplica aplica o limite de forma independente.

Qual status HTTP a API deve devolver ao estourar o limite?

O status 429 Too Many Requests, de preferência com o cabeçalho Retry-After indicando quando tentar novamente.

Glossário

Termo Significado
Rate limiting Técnica que limita o número de requisições em um intervalo de tempo.
Tenant Cliente de uma aplicação multi-tenant, com dados e regras próprios.
Multi-tenant Arquitetura em que vários clientes compartilham a mesma aplicação.
Partição Grupo de requisições que compartilha os mesmos contadores de limite.
Token bucket Algoritmo que repõe uma quantidade fixa de tokens por período.
Noisy neighbor Cliente que consome recursos demais e prejudica os demais.
Retry-After Cabeçalho HTTP que indica quando o cliente pode tentar de novo.
field Palavra-chave do C# 14 que acessa o campo de apoio de uma propriedade.
Extension members Recurso do C# 14 que permite criar propriedades de extensão.
Claim Informação sobre o usuário contida em um token autenticado.

Fontes