App-vNext/Polly · error · ArgumentOutOfRangeException

The retryAfter parameter must be a TimeSpan greater than or

Error message

The retryAfter parameter must be a TimeSpan greater than or equal to TimeSpan.Zero.

What it means

Thrown by RateLimitRejectedException.SetRetryAfter when retryAfter is less than TimeSpan.Zero. The exception's RetryAfter property must be a valid non-negative retry delay because callers use it to schedule the next attempt; a negative value would be meaningless.

Source

Thrown at src/Polly/RateLimit/RateLimitRejectedException.cs:92

        : base(message) => SetRetryAfter(retryAfter);

    /// <summary>
    /// Initializes a new instance of the <see cref="RateLimitRejectedException"/> class.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <param name="retryAfter">The timespan after which the operation may be retried.</param>
    /// <param name="innerException">The inner exception.</param>
    public RateLimitRejectedException(TimeSpan retryAfter, string message, Exception innerException)
        : base(message, innerException) => SetRetryAfter(retryAfter);

    private static string DefaultMessage(TimeSpan retryAfter) =>
        $"The operation has been rate-limited and should be retried after {retryAfter}";

    private void SetRetryAfter(TimeSpan retryAfter)
    {
        if (retryAfter < TimeSpan.Zero)
        {
            throw new ArgumentOutOfRangeException(nameof(retryAfter), retryAfter, $"The {nameof(retryAfter)} parameter must be a TimeSpan greater than or equal to TimeSpan.Zero.");
        }

        RetryAfter = retryAfter;
    }

#if NETSTANDARD2_0
    /// <summary>
    /// Initializes a new instance of the <see cref="RateLimitRejectedException"/> class.
    /// </summary>
    /// <param name="info">The information.</param>
    /// <param name="context">The context.</param>
    protected RateLimitRejectedException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
#endif
}

View on GitHub (pinned to d0e46bdb1e)

Solutions

  1. Clamp the computed retry delay with TimeSpan.Max(value, TimeSpan.Zero) before constructing the exception.
  2. Validate the source (e.g. Retry-After header) is non-negative before parsing to TimeSpan.
  3. If the delay is genuinely unknown, pass TimeSpan.Zero rather than a negative value.

Example fix

// before
var retryAfter = serverReset - DateTimeOffset.UtcNow;
throw new RateLimitRejectedException(retryAfter, msg, inner);
// after
var retryAfter = TimeSpan.Max(serverReset - DateTimeOffset.UtcNow, TimeSpan.Zero);
throw new RateLimitRejectedException(retryAfter, msg, inner);
Defensive patterns

Strategy: validation

Validate before calling

var retryAfter = serverReset - DateTimeOffset.UtcNow;
if (retryAfter < TimeSpan.Zero) retryAfter = TimeSpan.Zero;
throw new RateLimitRejectedException(retryAfter, message, inner);

Prevention

When it happens

Trigger: Constructing new RateLimitRejectedException(retryAfter, ...) (any overload chaining SetRetryAfter) with a TimeSpan that is negative. Typically happens when a custom retryAfterFactory or downstream code computes a delay that underflows.

Common situations: Clock skew or a subtracted offset producing a negative TimeSpan; a retry-after header parsed as a negative value; arithmetic on timestamps (e.g. serverTime - now) when now is ahead.

Related errors


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