App-vNext/Polly · error · ArgumentOutOfRangeException

perTimeSpan must be a positive timespan.

Error message

perTimeSpan must be a positive timespan.

What it means

Thrown by RateLimitTResultSyntax.RateLimit<TResult> when perTimeSpan is less than or equal to TimeSpan.Zero. The generic TResult overload requires a positive counting window, identical to the non-generic version.

Source

Thrown at src/Polly/RateLimit/RateLimitTResultSyntax.cs:73

    /// <param name="maxBurst">The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while).
    /// This equates to the bucket-capacity of a token-bucket implementation.</param>
    /// <param name="retryAfterFactory">An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited.
    /// <remarks>If null, a <see cref="RateLimitRejectedException"/> with property <see cref="RateLimitRejectedException.RetryAfter"/> will be thrown to indicate rate-limiting.</remarks></param>
    /// <returns>The policy instance.</returns>
    public static RateLimitPolicy<TResult> RateLimit<TResult>(
        int numberOfExecutions,
        TimeSpan perTimeSpan,
        int maxBurst,
        Func<TimeSpan, Context, TResult>? retryAfterFactory)
    {
        if (numberOfExecutions < 1)
        {
            throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), numberOfExecutions, $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1.");
        }

        if (perTimeSpan <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, $"{nameof(perTimeSpan)} must be a positive timespan.");
        }

        if (maxBurst < 1)
        {
            throw new ArgumentOutOfRangeException(nameof(maxBurst), maxBurst, $"{nameof(maxBurst)} must be an integer greater than or equal to 1.");
        }

        var onePer = TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions);

        if (onePer <= TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, "The number of executions per timespan must be positive.");
        }

        IRateLimiter rateLimiter = new LockFreeTokenBucketRateLimiter(onePer, maxBurst);

        return new RateLimitPolicy<TResult>(rateLimiter, retryAfterFactory);
    }

View on GitHub (pinned to d0e46bdb1e)

Solutions

  1. Set perTimeSpan to a positive TimeSpan.
  2. Enforce perTimeSpan > TimeSpan.Zero at config load.
  3. Verify the time unit (seconds vs milliseconds) is not collapsing to zero.

Example fix

// before
var policy = Policy.RateLimit<MyResult>(5, TimeSpan.Zero, 5, factory);
// after
var policy = Policy.RateLimit<MyResult>(5, TimeSpan.FromSeconds(10), 5, factory);
Defensive patterns

Strategy: validation

Validate before calling

var perTimeSpan = config.GetValue<TimeSpan>("RateLimit:Window");
if (perTimeSpan <= TimeSpan.Zero) throw new InvalidOperationException("RateLimit:Window must be positive.");
var policy = Policy.RateLimit<TResult>(numberOfExecutions, perTimeSpan, maxBurst, retryAfterFactory);

Prevention

When it happens

Trigger: Calling Policy.RateLimit<TResult>(n, TimeSpan.Zero, ...) or a negative window on the generic sync overload.

Common situations: Config-driven window set to 0 from a missing key or wrong unit; env override yielding zero.

Related errors


AI-assisted analysis of App-vNext/Polly@d0e46bdb1e (2026-08-13). Data as JSON: /api/errors/e341ab65d52438c7. Report an issue: GitHub.