Polly v8 resilience strategy for RateLimitHeaders. Provides rate limit header awareness for Polly resilience pipelines.
$ dotnet add package RateLimitHeaders.PollyA .NET library for parsing IETF RateLimit headers and enabling proactive rate limit awareness in HTTP clients.
RateLimit and RateLimit-Policy headers per draft-ietf-httpapi-ratelimit-headers-10# Core library (IHttpClientFactory integration)
dotnet add package RateLimitHeaders
# For Polly v8 resilience pipeline integration
dotnet add package RateLimitHeaders.Polly
services.AddHttpClient("MyApi")
.AddRateLimitAwareHandler(options =>
{
options.EnableProactiveThrottling = true;
options.QuotaLowThreshold = 0.1; // Warn at 10% remaining
options.OnQuotaLow = args =>
{
logger.LogWarning("Quota low: {Remaining}/{Quota}",
args.RateLimitInfo.Remaining,
args.RateLimitInfo.Quota);
return ValueTask.CompletedTask;
};
});
When using multiple delegating handlers, place the rate limit aware handler:
services.AddHttpClient("MyApi")
.AddHttpMessageHandler<AuthenticationHandler>() // 1. Auth first
.AddRateLimitAwareHandler() // 2. Rate limiting
.AddStandardResilienceHandler(); // 3. Retry/resilience last
var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
.AddRateLimitHeaders(options =>
{
options.EnableProactiveThrottling = true;
options.OnRateLimitInfo = args =>
{
logger.LogDebug("Rate limit: {Info}", args.RateLimitInfo);
return ValueTask.CompletedTask;
};
})
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>())
.Build();
// Create a context to capture rate limit info
var context = ResilienceContextPool.Shared.Get(cancellationToken);
try
{
var response = await pipeline.ExecuteAsync(
async (ctx, ct) => await httpClient.GetAsync("/api/resource", ct),
context,
cancellationToken);
// Access parsed rate limit info from context
if (context.Properties.TryGetValue(RateLimitContextProperties.RateLimitInfoKey, out var info))
{
Console.WriteLine($"Remaining: {info.Remaining}/{info.Quota}");
}
}
finally
{
ResilienceContextPool.Shared.Return(context);
}
Use the extension methods on HttpResponseMessage:
var response = await httpClient.GetAsync("/api/resource");
// Parse headers directly
if (response.TryGetRateLimitInfo(out var info))
{
Console.WriteLine($"Remaining: {info.Remaining}/{info.Quota}");
Console.WriteLine($"Resets in: {info.ResetSeconds}s");
if (info.IsQuotaLow(0.1))
Console.WriteLine("Warning: Quota is low!");
}
// Or get cached info when using RateLimitAwareHandler
if (response.TryGetStoredRateLimitInfo(out var cached))
{
Console.WriteLine($"Cached: {cached.Remaining}/{cached.Quota}");
}
This library parses the IETF standard format:
RateLimit: "default";r=50;t=30
RateLimit-Policy: "default";q=100;w=60
| Header | Format | Description |
|---|---|---|
RateLimit | "policy";r=remaining;t=reset | Current rate limit state |
RateLimit-Policy | "policy";q=quota;w=window | Rate limit policy definition |
| Property | Description |
|---|---|
PolicyName | The rate limit policy name (e.g., "default", "api-v2") |
Remaining | Requests remaining in the current window |
ResetSeconds | Seconds until the window resets |
Quota | Maximum requests allowed per window |
WindowSeconds | Duration of the rate limit window |
IsValid | Whether at least one header was successfully parsed |
When enabled, the handler automatically delays requests when quota is low:
// Default algorithm: PercentageThrottlingAlgorithm
// Starts throttling when remaining < 10% of quota
// Delay = (threshold - remainingPct) * resetSeconds * factor
// Max delay = 5 seconds
options.ThrottlingAlgorithm = new PercentageThrottlingAlgorithm(
threshold: 0.1, // Start at 10% remaining
factor: 1.0, // Delay multiplier
maxDelay: TimeSpan.FromSeconds(5));
Implement IThrottlingAlgorithm for custom strategies:
public class CustomThrottlingAlgorithm : IThrottlingAlgorithm
{
public ThrottlingResult Evaluate(RateLimitInfo info)
{
if (!info.IsValid || info.Remaining > 5)
return ThrottlingResult.NoThrottle;
var delay = TimeSpan.FromSeconds(info.ResetSeconds / 2.0);
return ThrottlingResult.Throttle(delay, "Custom throttle");
}
}
Called whenever rate limit headers are parsed:
options.OnRateLimitInfo = args =>
{
metrics.RecordRateLimit(
args.RateLimitInfo.PolicyName,
args.RateLimitInfo.Remaining,
args.RateLimitInfo.Quota);
return ValueTask.CompletedTask;
};
Called when remaining quota falls below threshold:
options.OnQuotaLow = args =>
{
alertService.SendAlert(
$"API quota at {args.RemainingPercentage:P0}",
args.RequestUri);
return ValueTask.CompletedTask;
};
Called before a request is throttled:
options.OnThrottling = args =>
{
logger.LogInformation(
"Throttling request to {Uri} for {Delay}ms: {Reason}",
args.RequestUri,
args.Delay.TotalMilliseconds,
args.Reason);
return ValueTask.CompletedTask;
};
By default, rate limit state is tracked per hostname:
// Default: hostname
// api.example.com/v1/users -> "api.example.com"
// api.example.com/v1/orders -> "api.example.com"
// Custom key extraction (e.g., include path segment):
options.StateKeyExtractor = request =>
{
var uri = request.RequestUri;
if (uri is null) return "default";
var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
return segments.Length > 0 ? $"{uri.Host}/{segments[0]}" : uri.Host;
};
Some APIs partition rate limits by tenant, user, or API key (indicated by the pk parameter in response headers). The library parses this value into RateLimitInfo.PartitionKey, but proactive throttling does not automatically use it because:
If your API uses partition-based rate limiting, use StateKeyExtractor to include the partition identifier from your request:
// For APIs that partition by API key header:
options.StateKeyExtractor = request =>
{
var apiKey = request.Headers.TryGetValues("X-API-Key", out var values)
? values.FirstOrDefault()
: null;
var host = request.RequestUri?.Host ?? "default";
return apiKey is not null ? $"{host}:{apiKey}" : host;
};
// For APIs that partition by tenant ID in the path:
options.StateKeyExtractor = request =>
{
var uri = request.RequestUri;
if (uri is null) return "default";
// Extract tenant from path like /api/tenants/{tenantId}/...
var match = Regex.Match(uri.AbsolutePath, @"/tenants/([^/]+)");
return match.Success ? $"{uri.Host}:{match.Groups[1].Value}" : uri.Host;
};
This ensures each partition is tracked independently for accurate proactive throttling.
Note: This library requires .NET 8.0 or later. Earlier framework versions are not supported.
| Framework | Supported |
|---|---|
| .NET 8.0 | Yes |
| .NET 9.0 | Yes |
| .NET 10.0 | Yes |
RateLimitHeaders (core):
RateLimitHeaders.Polly (additional):
The library is designed to be thread-safe:
RateLimitInfo is a readonly struct (immutable)ConcurrentDictionaryMIT License - see LICENSE for details.
Contributions are welcome! Please read our contributing guidelines and submit PRs to the develop branch.