So far, the policies we’ve looked at are global. If your BasicLimiter allows 100 requests per minute, and User A makes 100 requests, User B gets blocked even if they haven’t made a single request!

In the real world, especially when designing robust architectures like a multi-tenant Inventory Management System, this is a disaster. You need a way to isolate traffic so that one noisy tenant or user doesn’t exhaust the limits for everyone else.

This is solved by Partitioning.

Partitioning allows you to create separate, isolated rate limit counters based on a specific key—usually an IP address, a User ID, or a Tenant ID. If Tenant A burns through their 100 requests, only Tenant A gets the 429 error. Tenant B’s API calls will continue to process smoothly.

How to Implement Partitioning in .NET 10

Instead of adding a standard fixed window, we use AddPolicy to dynamically inspect the incoming HTTP request, extract our partition key (like an IP or a header), and apply the limit to that specific key.

Let’s look at a practical example where we rate limit by a Tenant-ID header:

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

// Inside builder.Services.AddRateLimiter(options => { ... })

options.AddPolicy("TenantBasedLimiter", httpContext =>
{
    // 1. Extract the key (e.g., from a custom header, a JWT claim, or IP Address)
    var tenantId = httpContext.Request.Headers["X-Tenant-ID"].ToString();
    
    // Fallback for requests without a tenant ID
    if (string.IsNullOrEmpty(tenantId))
    {
        tenantId = "anonymous_user"; 
    }

    // 2. Return a partitioned rate limiter
    return RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: tenantId, 
        factory: partition => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100, // Each tenant gets 100 requests
            Window = TimeSpan.FromMinutes(1), // Every 1 minute
            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
            QueueLimit = 0 // No queueing, reject immediately if over limit
        });
});

 

Rate Limiting by IP Address

If you are building a public-facing API without authentication, the most common approach is to partition by the client’s IP address. The setup is nearly identical; you just change how you grab the key:

options.AddPolicy("IpBasedLimiter", httpContext =>
{
    // Extract the client's IP Address
    var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown_ip";

    return RateLimitPartition.GetSlidingWindowLimiter(
        partitionKey: ipAddress,
        factory: partition => new SlidingWindowRateLimiterOptions
        {
            PermitLimit = 20,
            Window = TimeSpan.FromSeconds(30),
            SegmentsPerWindow = 3
        });
});

 

Applying the Partitioned Policy

Applying it to your endpoints remains exactly the same as the basic policies:

// Minimal APIs
app.MapGet("/api/inventory/stock", () => "Stock levels for your tenant")
   .RequireRateLimiting("TenantBasedLimiter");

// Controllers
[HttpGet("stock")]
[EnableRateLimiting("TenantBasedLimiter")]
public IActionResult GetStock()
{
    return Ok("Stock levels for your tenant");
}

 

Final Thoughts

Implementing rate limiting used to mean bringing in heavy external dependencies or writing complex, thread-safe concurrent dictionaries yourself. With .NET 10, Microsoft has handed us a highly optimized, incredibly flexible middleware right out of the box.

Whether you are protecting a simple public endpoint with an IP-based sliding window or managing a massive multi-tenant microservice cluster with token buckets, you now have the tools to keep your APIs fast, fair, and resilient under pressure.

Happy coding!