ManagedCode.Orleans.RateLimiting.Server 10.2.0

Prefix Reserved
dotnet add package ManagedCode.Orleans.RateLimiting.Server --version 10.2.0
                    
NuGet\Install-Package ManagedCode.Orleans.RateLimiting.Server -Version 10.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ManagedCode.Orleans.RateLimiting.Server" Version="10.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ManagedCode.Orleans.RateLimiting.Server" Version="10.2.0" />
                    
Directory.Packages.props
<PackageReference Include="ManagedCode.Orleans.RateLimiting.Server" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ManagedCode.Orleans.RateLimiting.Server --version 10.2.0
                    
#r "nuget: ManagedCode.Orleans.RateLimiting.Server, 10.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ManagedCode.Orleans.RateLimiting.Server@10.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ManagedCode.Orleans.RateLimiting.Server&version=10.2.0
                    
Install as a Cake Addin
#tool nuget:?package=ManagedCode.Orleans.RateLimiting.Server&version=10.2.0
                    
Install as a Cake Tool

ManagedCode.Orleans.RateLimiting

Distributed rate limiting for Microsoft Orleans applications.

The library wraps System.Threading.RateLimiting behind Orleans grains so the same limiter can be shared across silos, HTTP middleware, SignalR hubs, and grain calls.

Packages

  • ManagedCode.Orleans.RateLimiting.Core — shared grain contracts, attributes, options, leases, and Orleans serializers.
  • ManagedCode.Orleans.RateLimiting.Server — Orleans grain implementations, incoming grain call filters, and silo registration helpers.
  • ManagedCode.Orleans.RateLimiting.Client — Orleans client, ASP.NET Core middleware, SignalR, and HTTP attribute integration.

Features

  • Fixed window, sliding window, token bucket, and concurrency limiters.
  • Distributed limiter state through Orleans grains.
  • Durable quota snapshots through Orleans grain storage, with reset and state deletion support.
  • Grain method attributes for grain-call rate limiting.
  • Request orchestration for per-user, per-group, per-tenant, per-role, per-IP, per-endpoint, per-grain, and custom partitions.
  • ASP.NET Core request middleware plus controller attributes for IP, anonymous user, authorized user, and role-aware limiting.
  • SignalR hub invocation filtering through the same request orchestration pipeline.
  • Central package management and .NET 10 build/test/coverage workflow.

Requirements

  • .NET SDK 10
  • Microsoft Orleans 10

Installation

Install the server package in the silo host:

dotnet add package ManagedCode.Orleans.RateLimiting.Server

Install the client package in applications that call limiters or use ASP.NET Core middleware:

dotnet add package ManagedCode.Orleans.RateLimiting.Client

Silo Setup

Register the Orleans rate-limiting services, configure the package storage provider, and add any limiter defaults that should be enforced by grain call filters.

using ManagedCode.Orleans.RateLimiting.Server;
using ManagedCode.Orleans.RateLimiting.Server.Extensions;
using ManagedCode.Orleans.RateLimiting.Server.Options;

siloBuilder.AddAzureTableGrainStorage(
    RateLimiterStorageNames.StorageProviderName,
    options =>
    {
        options.ConfigureTableServiceClient(connectionString);
        options.DeleteStateOnClear = true;
    });

siloBuilder.AddOrleansRateLimiting();
siloBuilder.Services.Configure<RateLimiterPersistenceOptions>(options =>
{
    options.StateFlushPeriod = TimeSpan.FromMinutes(5);
});

siloBuilder.AddOrleansConcurrencyLimiter(options =>
{
    options.PermitLimit = 10;
    options.QueueLimit = 15;
});

siloBuilder.AddOrleansFixedWindowRateLimiter(options =>
{
    options.PermitLimit = 10;
    options.QueueLimit = 15;
    options.Window = TimeSpan.FromSeconds(1);
});

siloBuilder.AddOrleansSlidingWindowRateLimiter(options =>
{
    options.PermitLimit = 10;
    options.QueueLimit = 15;
    options.Window = TimeSpan.FromSeconds(1);
    options.SegmentsPerWindow = 2;
});

siloBuilder.AddOrleansTokenBucketRateLimiter(options =>
{
    options.TokenLimit = 10;
    options.QueueLimit = 15;
    options.TokensPerPeriod = 2;
    options.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
});

Durable State Storage

Rate limiter grains persist their configuration and quota snapshot through Orleans grain storage. Register the storage provider before AddOrleansRateLimiting() and always use RateLimiterStorageNames.StorageProviderName from the ManagedCode.Orleans.RateLimiting.Server namespace:

siloBuilder.AddAzureTableGrainStorage(
    RateLimiterStorageNames.StorageProviderName,
    options =>
    {
        options.ConfigureTableServiceClient(connectionString);
        options.DeleteStateOnClear = true;
    });

For local development or tests, use the same provider name with an in-memory provider:

siloBuilder.AddMemoryGrainStorage(RateLimiterStorageNames.StorageProviderName);

RateLimiterPersistenceDefaults.StorageProviderName remains available as a compatibility alias, but new host setup should use RateLimiterStorageNames.StorageProviderName.

Limiter grains update in-memory state on acquire/release and flush changed state to Orleans storage every five minutes by default. They also force a write during configuration, reset, and activation deactivation. Configure the flush interval when tests or workloads need a different value:

siloBuilder.Services.Configure<RateLimiterPersistenceOptions>(options =>
{
    options.StateFlushPeriod = TimeSpan.FromSeconds(30);
});

State cleanup has two different operations:

var limiter = clusterClient.GetFixedWindowRateLimiter("tenant:api");

await limiter.ResetAsync();
await limiter.DeleteStateAsync();

ResetAsync() clears the current quota window or active concurrency leases while keeping the limiter configuration in storage. Use it for admin reset flows where the limiter still exists.

DeleteStateAsync() clears the Orleans persistent state record through IPersistentState.ClearStateAsync(). It removes the stored limiter configuration, quota snapshot, counters, and active lease state, then returns the in-memory activation to the silo defaults. Use it when the limiter key is no longer needed. Physical deletion is provider-defined; Azure Table and Blob storage delete the row/blob when their DeleteStateOnClear option is enabled.

Direct Limiter Usage

Limiters are available as extensions on IGrainFactory and IClusterClient.

var limiter = clusterClient.GetConcurrencyLimiter("tenant:user");

await limiter.Configure(new ConcurrencyLimiterOptions
{
    PermitLimit = 20,
    QueueLimit = 40,
    QueueProcessingOrder = QueueProcessingOrder.OldestFirst
});

await using var lease = await limiter.AcquireAsync();
if (!lease.IsAcquired)
{
    Console.WriteLine(lease.Reason);
    Console.WriteLine(lease.RetryAfter);
    return;
}

// Continue protected work.

You can also create option-backed holders when configuration should be checked before acquisition:

var limiter = clusterClient.GetFixedWindowRateLimiter(
    "tenant:api",
    new FixedWindowRateLimiterOptions
    {
        PermitLimit = 100,
        QueueLimit = 0,
        Window = TimeSpan.FromMinutes(1)
    });

await using var lease = await limiter.AcquireAndConfigureAsync();
lease.ThrowIfNotAcquired();

Reset the durable quota state through the limiter holder when an administrative workflow needs to clear the current limiter window or active concurrency leases:

await limiter.ResetAsync();

Delete the durable state when the limiter key should be removed completely and future calls should start from silo defaults:

await limiter.DeleteStateAsync();

Request Orchestration

Request orchestration maps one logical request into one or more distributed limiter partitions. This is the preferred API for new applications because it lets a cluster enforce combined limits such as per user, per group, per tenant, per endpoint, and custom metadata keys.

builder.Services.AddOrleansRateLimiting(options =>
{
    options.AddUser("user-checkout", required: true);
    options.AddGroup("group-checkout");
    options.AddTenant("tenant-checkout");
    options.AddCustom("route-checkout", metadataKey: "route");
});

Rules without a policy name apply to the default request pipeline. Use named policies when different surfaces need independent limits.

builder.Services.AddOrleansRateLimiting(options =>
{
    options.AddToPolicy("checkout-api", RateLimitPartitionKind.User, "user-checkout", required: true);
    options.AddToPolicy("checkout-api", RateLimitPartitionKind.Group, "group-checkout");
});

app.UseOrleansRequestRateLimiting("checkout-api");

The default IRateLimitRequestOrchestrator uses registered IRateLimitRequestPolicy instances, IRateLimitRequestKeyResolver, named RateLimiterConfig values, and Orleans grains to build a GroupLimiterHolder.

var request = new RateLimitRequestContext
{
    OperationName = "checkout",
    PolicyName = "checkout-api",
    UserId = "user-123",
    GroupId = "group-a",
    TenantId = "tenant-main",
    Metadata = new Dictionary<string, string>
    {
        ["route"] = "/checkout"
    }
};

await using var group = await orchestrator.CreateLimiterGroupAsync(request);
var rejectedLease = await group.AcquireAsync();
if (rejectedLease is not null)
{
    throw rejectedLease.ToException();
}

Implement custom IRateLimitRequestPolicy or IRateLimitRequestKeyResolver when the default context fields are not enough.

Grain Attributes

Decorate grain methods to enforce rate limiting through incoming grain call filters.

public class TestFixedWindowRateLimiterGrain : Grain, ITestFixedWindowRateLimiterGrain
{
    [FixedWindowRateLimiter]
    public Task<string> ByGrainId()
    {
        return Task.FromResult("ok");
    }

    [FixedWindowRateLimiter(KeyType.Key, "shared-key")]
    public Task<string> BySharedKey()
    {
        return Task.FromResult("ok");
    }

    [FixedWindowRateLimiter(KeyType.GrainType, permitLimit: 2, queueLimit: 1)]
    public Task<string> ByGrainType()
    {
        return Task.FromResult("ok");
    }
}

Grain attribute policy safety (10.2.0)

Grain filters honor attributes on implementation methods, then interface methods, then the implementation class. Explicit key partitions require a non-empty key; an empty or missing named configuration fails before protected work executes. Different named policies and different inline option sets use separate limiter identities, preventing one operation from resetting another operation's quota. Identical inline options retain shared quota for the selected key. These changes start new counters for named/inline policies; deploy clients and silos consistently.

GroupLimiterHolder rejects overlapping acquisition and changes to its members while acquisition/lease ownership is active. It rolls back earlier permits if a later acquisition fails and waits for pending acquisition during disposal. Build a group before acquiring it and continue to use await using around protected work.

Invalid configuration updates and invalid permit counts leave the existing limiter, quota, and active concurrency leases intact. Correct the rejected input before retrying.

ASP.NET Core Usage

Register named limiter options and configure request orchestration rules.

builder.Services.AddOrleansRateLimiterOptions("ip", new FixedWindowRateLimiterOptions
{
    QueueLimit = 5,
    PermitLimit = 10,
    Window = TimeSpan.FromSeconds(1)
});

builder.Services.AddOrleansRateLimiterOptions("Anonymous", new FixedWindowRateLimiterOptions
{
    QueueLimit = 1,
    PermitLimit = 1,
    Window = TimeSpan.FromSeconds(1)
});

builder.Services.AddOrleansRateLimiterOptions("Authorized", new FixedWindowRateLimiterOptions
{
    QueueLimit = 2,
    PermitLimit = 2,
    Window = TimeSpan.FromSeconds(1)
});

builder.Services.AddOrleansRateLimiting(options =>
{
    options.AddIpAddress("ip");
    options.AddUser("Authorized");
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseOrleansRequestRateLimiting();

// Attribute-based middleware is still available for existing controllers.
app.UseOrleansIpRateLimiting();
app.UseOrleansUserRateLimiting();

app.MapControllers();

Cancellation and metrics (10.2.0)

HTTP middleware propagates RequestAborted; SignalR propagates ConnectionAborted. Queued requests are cancelled in Orleans and the native limiter, and partial groups release concurrency leases. Direct holders also accept a token:

await using var lease = await limiter.AcquireAsync(permitCount: 1, cancellationToken);

Deploy updated silos before callers of the new cancellation APIs. Existing methods and RPC identities remain available. Custom legacy holders remain compatible but cannot interrupt an acquisition unless they implement ICancellableLimiterHolder. Subscribe to meter ManagedCode.Orleans.RateLimiting for acquisition outcomes/durations, active concurrency permits, and storage writes/durations. Metric tags contain no partition identities. See cancellation, metrics and persistence details.

Client IPs and trusted proxies (10.2.0)

HTTP middleware and SignalR use HttpContext.Connection.RemoteIpAddress. Raw X-Forwarded-For, X-Real-IP, and REMOTE_ADDR headers do not select rate-limit partitions. IPv4-mapped IPv6 addresses are normalized to IPv4; requests without a connection address share the unknown-ip partition instead of skipping IP limits.

Behind a reverse proxy, configure ASP.NET Core forwarded headers with your actual trusted proxy addresses, and run it before authentication and rate limiting:

using System.Net;
using Microsoft.AspNetCore.HttpOverrides;

const string trustedProxyAddress = "10.0.0.10"; // Replace with your proxy's address.
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor;
    options.KnownProxies.Add(IPAddress.Parse(trustedProxyAddress));
});

// After building the app, before the pipeline shown above:
app.UseForwardedHeaders();

Keep a bounded ForwardLimit matching your proxy chain and restrict trusted proxies/networks. Do not clear both trust lists or trust arbitrary clients. See Microsoft's proxy configuration guide. The legacy GetClientIpAddress(headers) overload explicitly trusts the supplied headers and is for callers that independently enforce this boundary; built-in middleware never calls it.

Attribute middleware now throws RateLimitConfigurationNotFoundException when a referenced configuration is missing, before running the protected endpoint. Attribute keys also include configuration identity so stacked policies cannot reset each other. These key corrections start new counters during upgrade; deploy all application nodes consistently. Use stable authenticated identifiers for user/tenant/custom policies, mark required partitions required: true, and combine them with an IP or shared limit where anonymous requests must also be limited. Apply authentication before claim-based rules. Orleans cluster access, configuration/reset/delete methods, and lease IDs are trusted application capabilities; do not expose them directly to HTTP clients. See the 10.2.0 security review for scope and verification.

Apply HTTP limiter attributes to controllers or actions.

[AuthorizedIpRateLimiter("Authorized")]
[AnonymousIpRateLimiter("Anonymous")]
[InRoleIpRateLimiter("Authorized", "Admin")]
[HttpGet("limited")]
public Task<ActionResult<string>> GetLimited()
{
    return Task.FromResult<ActionResult<string>>("OK");
}

SignalR Usage

SignalR hub methods can be rate limited through the same request orchestration infrastructure.

builder.Services.AddOrleansRateLimiterOptions("SignalR", new FixedWindowRateLimiterOptions
{
    PermitLimit = 10,
    QueueLimit = 0,
    Window = TimeSpan.FromSeconds(1)
});

builder.Services
    .AddSignalR()
    .AddOrleansRateLimiting("SignalR", RateLimitPartitionKind.User);

The built-in hub filter uses a named SignalR policy by default, so hub rules do not bleed into HTTP request middleware rules. It builds a RateLimitRequestContext from hub method name, user identifier, claims, IP address, and hub resource name.

Development

This repository targets .NET 10 with central package management.

dotnet restore ManagedCode.Orleans.RateLimiting.sln
dotnet build ManagedCode.Orleans.RateLimiting.sln --configuration Release --no-restore
dotnet test --solution ManagedCode.Orleans.RateLimiting.sln --configuration Release --no-build --verbosity normal
dotnet format ManagedCode.Orleans.RateLimiting.sln --verify-no-changes

Coverage uses local tools from .config/dotnet-tools.json.

dotnet tool restore
dotnet tool run coverlet ManagedCode.Orleans.RateLimiting.Tests/bin/Release/net10.0/ManagedCode.Orleans.RateLimiting.Tests.dll --target "dotnet" --targetargs "test --project ManagedCode.Orleans.RateLimiting.Tests/ManagedCode.Orleans.RateLimiting.Tests.csproj --configuration Release --no-build --no-restore" --format cobertura --output artifacts/coverage/coverage.cobertura.xml --exclude "[ManagedCode.Orleans.RateLimiting.Tests]*" --threshold 85 --threshold-type line --threshold-stat total
dotnet tool run reportgenerator -reports:"artifacts/coverage/coverage.cobertura.xml" -targetdir:"artifacts/coverage-report" -reporttypes:"HtmlSummary;MarkdownSummaryGithub"

Current local coverage after the .NET 10 migration and request-orchestration refactor is above the 85% line-coverage target.

Contributing

Issues and pull requests are welcome in the GitHub repository.

Acquisition deadlines and performance

Register AddOrleansRateLimiting() on the client and silo. Version 10.2 holder acquisitions use server-bounded RPCs; deploy updated silos before clients. The client supplies a wait budget of 80% of the Orleans response timeout. The configured steady-state fast path creates no deadline timer and uses distributed cancellation only for a cancellable caller token. Queued/configuration waits enforce their remaining budget locally on the server. The bounded proxy supplies this budget without a global outgoing filter, preserving Orleans' direct dispatch path for release and unrelated grain calls. Deadlines return rejected leases; caller cancellation throws OperationCanceledException.

Fixed-window, sliding-window and token-bucket leases omit unnecessary release RPCs and server retention. Concurrency disposal reuses the holder's grain reference to return permits. Empty lease metadata shares immutable storage. Global Orleans timeout settings are unchanged; network failures still do not provide distributed concurrency-lease expiry guarantees.

See performance and timeout validation for measured throughput, p99, allocations, real-token comparisons, and reproducible benchmark commands.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ManagedCode.Orleans.RateLimiting.Server:

Package Downloads
ManagedCode.Communication.Orleans

Orleans serialization, filters, idempotency, and distributed command rate limiting for ManagedCode.Communication.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on ManagedCode.Orleans.RateLimiting.Server:

Repository Stars
managedcode/Communication
Result pattern for .NET that replaces exceptions with type-safe return values. Features railway-oriented programming, ASP.NET Core integration, RFC 7807 Problem Details, and built-in pagination. Designed for production systems requiring explicit error handling without the overhead of throwing exceptions.
Version Downloads Last Updated
10.2.0 22 9/7/2026
10.1.0 5,667 5/10/2026
10.0.0 990 5/5/2026
0.0.9 231 11/7/2025
0.0.8 325 5/13/2024
0.0.6 313 5/24/2023
0.0.3 353 4/17/2023
0.0.1 324 4/15/2023