Table of Contents

Custom Throttling Algorithms

RateLimitHeaders allows you to implement custom throttling algorithms for specialized rate limiting strategies.

The IThrottlingAlgorithm Interface

public interface IThrottlingAlgorithm
{
    // Simple evaluation based on current endpoint's rate limit info
    ThrottlingResult Evaluate(RateLimitInfo rateLimitInfo);

    // Advanced evaluation with access to all tracked endpoints (has default implementation)
    ThrottlingResult Evaluate(RateLimitInfo rateLimitInfo, IRateLimitStateProvider? stateProvider)
        => Evaluate(rateLimitInfo);
}

public interface IRateLimitStateProvider
{
    RateLimitInfo GetRateLimitInfo(string endpointKey);
    IEnumerable<string> TrackedEndpoints { get; }
    IEnumerable<RateLimitInfo> GetAllStates();
}

public readonly struct ThrottlingResult
{
    public bool ShouldThrottle { get; }
    public TimeSpan Delay { get; }
    public string? Reason { get; }

    public static ThrottlingResult NoThrottle { get; }
    public static ThrottlingResult Throttle(TimeSpan delay, string? reason = null);
}

Most custom algorithms only need to implement the simple Evaluate(RateLimitInfo) method. The overload with IRateLimitStateProvider is for advanced scenarios where you need cross-endpoint visibility (e.g., global throttling across multiple API endpoints).

Implementing a Custom Algorithm

Fixed Threshold Algorithm

Throttle when remaining requests fall below a fixed number:

public class FixedThresholdAlgorithm : IThrottlingAlgorithm
{
    private readonly int _minRemaining;
    private readonly TimeSpan _delay;

    public FixedThresholdAlgorithm(int minRemaining = 5, TimeSpan? delay = null)
    {
        _minRemaining = minRemaining;
        _delay = delay ?? TimeSpan.FromSeconds(1);
    }

    public ThrottlingResult Evaluate(RateLimitInfo info)
    {
        if (!info.IsValid || info.Remaining > _minRemaining)
            return ThrottlingResult.NoThrottle;

        return ThrottlingResult.Throttle(
            _delay,
            $"Only {info.Remaining} requests remaining");
    }
}

Adaptive Delay Algorithm

Scale delay based on how close you are to exhausting quota:

public class AdaptiveDelayAlgorithm : IThrottlingAlgorithm
{
    private readonly double _threshold;
    private readonly TimeSpan _minDelay;
    private readonly TimeSpan _maxDelay;

    public AdaptiveDelayAlgorithm(
        double threshold = 0.2,
        TimeSpan? minDelay = null,
        TimeSpan? maxDelay = null)
    {
        _threshold = threshold;
        _minDelay = minDelay ?? TimeSpan.FromMilliseconds(100);
        _maxDelay = maxDelay ?? TimeSpan.FromSeconds(10);
    }

    public ThrottlingResult Evaluate(RateLimitInfo info)
    {
        if (!info.IsValid || info.Quota <= 0)
            return ThrottlingResult.NoThrottle;

        var remainingPct = (double)info.Remaining / info.Quota;
        if (remainingPct > _threshold)
            return ThrottlingResult.NoThrottle;

        // Scale delay: lower remaining = longer delay
        var severity = 1.0 - (remainingPct / _threshold);
        var delayRange = _maxDelay - _minDelay;
        var delay = _minDelay + TimeSpan.FromTicks((long)(delayRange.Ticks * severity));

        return ThrottlingResult.Throttle(
            delay,
            $"Quota at {remainingPct:P0}, delaying {delay.TotalMilliseconds:F0}ms");
    }
}

Token Bucket Algorithm

Spread remaining requests evenly across the time window:

public class TokenBucketAlgorithm : IThrottlingAlgorithm
{
    private readonly int _reserveTokens;

    public TokenBucketAlgorithm(int reserveTokens = 2)
    {
        _reserveTokens = reserveTokens;
    }

    public ThrottlingResult Evaluate(RateLimitInfo info)
    {
        if (!info.IsValid || info.Remaining <= _reserveTokens)
        {
            if (info.ResetSeconds > 0)
            {
                return ThrottlingResult.Throttle(
                    TimeSpan.FromSeconds(info.ResetSeconds),
                    $"Quota exhausted, waiting for reset");
            }
            return ThrottlingResult.NoThrottle;
        }

        // Calculate ideal spacing between requests
        var availableTokens = info.Remaining - _reserveTokens;
        if (availableTokens <= 0 || info.ResetSeconds <= 0)
            return ThrottlingResult.NoThrottle;

        var spacing = TimeSpan.FromSeconds(info.ResetSeconds / (double)availableTokens);

        // Only throttle if spacing is significant
        if (spacing.TotalMilliseconds < 50)
            return ThrottlingResult.NoThrottle;

        return ThrottlingResult.Throttle(
            spacing,
            $"Pacing requests: {availableTokens} requests over {info.ResetSeconds}s");
    }
}

Using Custom Algorithms

With IHttpClientFactory

services.AddHttpClient("MyApi")
    .AddRateLimitAwareHandler(options =>
    {
        options.EnableProactiveThrottling = true;
        options.ThrottlingAlgorithm = new AdaptiveDelayAlgorithm(
            threshold: 0.3,
            maxDelay: TimeSpan.FromSeconds(5));
    });

With Polly

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRateLimitHeaders(options =>
    {
        options.EnableProactiveThrottling = true;
        options.ThrottlingAlgorithm = new TokenBucketAlgorithm(reserveTokens: 5);
    })
    .Build();

Combining Algorithms

Create a composite algorithm that uses different strategies:

public class CompositeThrottlingAlgorithm : IThrottlingAlgorithm
{
    private readonly IThrottlingAlgorithm _primary;
    private readonly IThrottlingAlgorithm _fallback;

    public CompositeThrottlingAlgorithm(
        IThrottlingAlgorithm primary,
        IThrottlingAlgorithm fallback)
    {
        _primary = primary;
        _fallback = fallback;
    }

    public ThrottlingResult Evaluate(RateLimitInfo info)
    {
        var result = _primary.Evaluate(info);
        if (result.ShouldThrottle)
            return result;

        return _fallback.Evaluate(info);
    }
}

Global Throttling with IRateLimitStateProvider

For scenarios where you need to make throttling decisions based on multiple endpoints (e.g., shared API quotas), override the Evaluate method that receives IRateLimitStateProvider:

public class GlobalThrottlingAlgorithm : IThrottlingAlgorithm
{
    private readonly double _globalThreshold;

    public GlobalThrottlingAlgorithm(double globalThreshold = 0.15)
    {
        _globalThreshold = globalThreshold;
    }

    public ThrottlingResult Evaluate(RateLimitInfo rateLimitInfo)
    {
        // Fallback when no state provider is available
        return EvaluateSingle(rateLimitInfo);
    }

    public ThrottlingResult Evaluate(RateLimitInfo rateLimitInfo, IRateLimitStateProvider? stateProvider)
    {
        if (stateProvider == null)
            return EvaluateSingle(rateLimitInfo);

        // Consider the lowest quota across all tracked endpoints
        var allStates = stateProvider.GetAllStates().Where(s => s.IsValid).ToList();
        if (allStates.Count == 0)
            return ThrottlingResult.NoThrottle;

        var lowestRemaining = allStates.Min(s => s.GetRemainingPercentage());
        if (lowestRemaining > _globalThreshold)
            return ThrottlingResult.NoThrottle;

        var maxReset = allStates.Max(s => s.ResetSeconds);
        var delay = TimeSpan.FromSeconds(maxReset * (1 - lowestRemaining / _globalThreshold));

        return ThrottlingResult.Throttle(
            delay,
            $"Global quota at {lowestRemaining:P0} across {allStates.Count} endpoints");
    }

    private ThrottlingResult EvaluateSingle(RateLimitInfo info)
    {
        if (!info.IsValid || info.GetRemainingPercentage() > _globalThreshold)
            return ThrottlingResult.NoThrottle;

        return ThrottlingResult.Throttle(
            TimeSpan.FromSeconds(info.ResetSeconds * 0.5),
            $"Single endpoint at {info.GetRemainingPercentage():P0}");
    }
}

Algorithm Selection Guidelines

Scenario Recommended Algorithm
General purpose PercentageThrottlingAlgorithm (default)
Bursty traffic with quiet periods FixedThresholdAlgorithm
Steady request stream TokenBucketAlgorithm
Unpredictable load patterns AdaptiveDelayAlgorithm
Mission-critical APIs Composite with conservative fallback
Shared quotas across endpoints Custom with IRateLimitStateProvider

Testing Custom Algorithms

[Fact]
public void AdaptiveDelay_WhenQuotaLow_ReturnsScaledDelay()
{
    var algorithm = new AdaptiveDelayAlgorithm(threshold: 0.2);

    var info = new RateLimitInfo
    {
        IsValid = true,
        Remaining = 5,
        Quota = 100,
        ResetSeconds = 60
    };

    var result = algorithm.Evaluate(info);

    Assert.True(result.ShouldThrottle);
    Assert.True(result.Delay > TimeSpan.Zero);
}