The Polly.RateLimiting package provides rate limiting capabilities using the .NET System.Threading.RateLimiting library, allowing you to control the rate of executions through your resilience pipelines.
Installation
dotnet add package Polly.RateLimiting
Extension Methods
AddRateLimiter
Adds a rate limiter to the resilience pipeline.
public static TBuilder AddRateLimiter<TBuilder>(
this TBuilder builder,
RateLimiterStrategyOptions options)
where TBuilder : ResiliencePipelineBuilderBase
options
RateLimiterStrategyOptions
required
The rate limiter strategy options
Returns: The builder instance with the rate limiter added.
Example:
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new RateLimiterStrategyOptions
{
RateLimiter = args =>
{
return myRateLimiter.AcquireAsync(cancellationToken: args.Context.CancellationToken);
},
OnRejected = args =>
{
Console.WriteLine("Rate limit exceeded");
return default;
}
})
.Build();
AddRateLimiter (with RateLimiter instance)
Adds a rate limiter instance directly to the pipeline.
public static TBuilder AddRateLimiter<TBuilder>(
this TBuilder builder,
RateLimiter limiter)
where TBuilder : ResiliencePipelineBuilderBase
Example:
var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 100,
QueueLimit = 50
});
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(limiter)
.Build();
AddConcurrencyLimiter
Adds a concurrency limiter to the pipeline.
public static TBuilder AddConcurrencyLimiter<TBuilder>(
this TBuilder builder,
int permitLimit,
int queueLimit = 0)
where TBuilder : ResiliencePipelineBuilderBase
Maximum number of permits that can be leased concurrently
Maximum number of permits that can be queued concurrently
Example:
var pipeline = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(permitLimit: 100, queueLimit: 50)
.Build();
AddConcurrencyLimiter (with options)
Adds a concurrency limiter with detailed options.
public static TBuilder AddConcurrencyLimiter<TBuilder>(
this TBuilder builder,
ConcurrencyLimiterOptions options)
where TBuilder : ResiliencePipelineBuilderBase
options
ConcurrencyLimiterOptions
required
The concurrency limiter options
Example:
var pipeline = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 100,
QueueLimit = 50,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst
})
.Build();
Configuration Classes
RateLimiterStrategyOptions
Options for configuring the rate limiter strategy.
public class RateLimiterStrategyOptions : ResilienceStrategyOptions
{
public Func<RateLimiterArguments, ValueTask<RateLimitLease>>? RateLimiter { get; set; }
public ConcurrencyLimiterOptions DefaultRateLimiterOptions { get; set; }
public Func<OnRateLimiterRejectedArguments, ValueTask>? OnRejected { get; set; }
}
Properties
RateLimiter
A delegate that produces a RateLimitLease. If null, the strategy will use a ConcurrencyLimiter created with DefaultRateLimiterOptions.
- Type:
Func<RateLimiterArguments, ValueTask<RateLimitLease>>?
- Default:
null
DefaultRateLimiterOptions
Options for the default concurrency limiter used when RateLimiter is null.
- Type:
ConcurrencyLimiterOptions
- Default:
PermitLimit = 1000, QueueLimit = 0
OnRejected
An event raised when execution is rejected by the rate limiter.
- Type:
Func<OnRateLimiterRejectedArguments, ValueTask>?
- Default:
null
Example:
var options = new RateLimiterStrategyOptions
{
DefaultRateLimiterOptions = new ConcurrencyLimiterOptions
{
PermitLimit = 100,
QueueLimit = 20
},
OnRejected = args =>
{
var retryAfter = args.Lease.TryGetMetadata(MetadataName.RetryAfter, out var metadata)
? metadata
: null;
Console.WriteLine($"Rate limited. Retry after: {retryAfter}");
return default;
}
};
RateLimiterArguments
Arguments passed to the rate limiter delegate.
public readonly struct RateLimiterArguments
{
public ResilienceContext Context { get; }
}
Example:
RateLimiter = args =>
{
// Access the resilience context
var operationKey = args.Context.OperationKey;
return myLimiter.AcquireAsync(cancellationToken: args.Context.CancellationToken);
}
OnRateLimiterRejectedArguments
Arguments passed to the OnRejected callback.
public readonly struct OnRateLimiterRejectedArguments
{
public ResilienceContext Context { get; }
public RateLimitLease Lease { get; }
}
Properties
Context
The resilience context associated with the execution.
Lease
The lease that was rejected by the rate limiter (has no permits).
Example:
OnRejected = args =>
{
if (args.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
Console.WriteLine($"Retry after: {retryAfter}");
}
return default;
}
Exceptions
RateLimiterRejectedException
Exception thrown when a rate limiter rejects an execution.
public sealed class RateLimiterRejectedException : ExecutionRejectedException
{
public TimeSpan? RetryAfter { get; }
}
Properties
RetryAfter
The amount of time to wait before retrying again. Retrieved from the RateLimitLease metadata.
- Type:
TimeSpan?
- Default:
null
Constructors
public RateLimiterRejectedException()
public RateLimiterRejectedException(TimeSpan retryAfter)
public RateLimiterRejectedException(string message)
public RateLimiterRejectedException(string message, TimeSpan retryAfter)
public RateLimiterRejectedException(string message, Exception inner)
public RateLimiterRejectedException(string message, TimeSpan retryAfter, Exception inner)
Example:
try
{
await pipeline.ExecuteAsync(async ct =>
{
// Your code here
});
}
catch (RateLimiterRejectedException ex)
{
if (ex.RetryAfter.HasValue)
{
Console.WriteLine($"Rate limited. Retry after {ex.RetryAfter.Value}");
await Task.Delay(ex.RetryAfter.Value);
}
}
Usage Examples
Basic Concurrency Limiting
var pipeline = new ResiliencePipelineBuilder()
.AddConcurrencyLimiter(permitLimit: 10, queueLimit: 5)
.Build();
// Execute work through the pipeline
await pipeline.ExecuteAsync(async ct =>
{
// Only 10 concurrent executions allowed
await DoWorkAsync(ct);
});
Custom Rate Limiter
var slidingWindow = new SlidingWindowRateLimiter(new SlidingWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 6
});
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(slidingWindow)
.Build();
Rate Limiting with Rejection Handling
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new RateLimiterStrategyOptions
{
DefaultRateLimiterOptions = new ConcurrencyLimiterOptions
{
PermitLimit = 50,
QueueLimit = 100
},
OnRejected = args =>
{
logger.LogWarning("Request rate limited for operation: {OperationKey}",
args.Context.OperationKey);
if (args.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
args.Context.Properties.Set(new ResiliencePropertyKey<TimeSpan>("RetryAfter"), retryAfter);
}
return default;
}
})
.Build();
Per-User Rate Limiting
var userLimiters = new ConcurrentDictionary<string, RateLimiter>();
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new RateLimiterStrategyOptions
{
RateLimiter = args =>
{
var userId = args.Context.Properties.GetValue(
new ResiliencePropertyKey<string>("UserId"),
"anonymous");
var limiter = userLimiters.GetOrAdd(userId, _ =>
new ConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 10,
QueueLimit = 5
}));
return limiter.AcquireAsync(cancellationToken: args.Context.CancellationToken);
}
})
.Build();
// Use with context
var context = ResilienceContextPool.Shared.Get();
context.Properties.Set(new ResiliencePropertyKey<string>("UserId"), "user123");
await pipeline.ExecuteAsync(async ct =>
{
// Rate limited per user
}, context);
Combining with Dependency Injection
services.AddResiliencePipeline("api-limiter", builder =>
{
builder.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 100,
QueueLimit = 50
});
});
// Use in a service
public class ApiService
{
private readonly ResiliencePipeline _pipeline;
public ApiService(ResiliencePipelineProvider<string> provider)
{
_pipeline = provider.GetPipeline("api-limiter");
}
public async Task<string> CallApiAsync()
{
return await _pipeline.ExecuteAsync(async ct =>
{
return await httpClient.GetStringAsync("/api/data", ct);
});
}
}
See Also