App-vNext/Polly · warning · RateLimitRejectedException

The operation has been rate-limited and should be retried af

Error message

The operation has been rate-limited and should be retried after {retryAfter}

What it means

Thrown by AsyncRateLimitEngine.ImplementationAsync when the configured rate limiter denies execution (PermitExecution returns false) and no retryAfterFactory was supplied. The RateLimitRejectedException carries a RetryAfter timespan computed by the token-bucket limiter indicating when retry becomes permissible. It is a deliberate rejection signal, not a bug.

Source

Thrown at src/Polly/RateLimit/AsyncRateLimitEngine.cs:26

        Func<TimeSpan, Context, TResult>? retryAfterFactory,
        Func<Context, CancellationToken, Task<TResult>> action,
        Context context,
        bool continueOnCapturedContext,
        CancellationToken cancellationToken)
    {
        (bool permit, TimeSpan retryAfter) = rateLimiter.PermitExecution();

        if (permit)
        {
            return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext);
        }

        if (retryAfterFactory != null)
        {
            return retryAfterFactory(retryAfter, context);
        }

        throw new RateLimitRejectedException(retryAfter);
    }
}

View on GitHub (pinned to d0e46bdb1e)

Solutions

  1. Wrap the rate-limited policy in a Retry policy with backoff that respects RetryAfter to ride out throttling.
  2. Provide a retryAfterFactory in RateLimitAsync to return a fallback TResult instead of throwing.
  3. Increase numberOfExecutions and/or maxBurst after measuring actual throughput needs.
  4. Catch RateLimitRejectedException at the call site and back off using exception.RetryAfter.

Example fix

// before
var rl = Policy.RateLimitAsync(10, TimeSpan.FromSeconds(1), 10);
await rl.ExecuteAsync(ctx => doWork(ctx), new Context(), CancellationToken.None);

// after
var retry = Policy.Handle<RateLimitRejectedException>()
    .WaitAndRetryAsync((_, ex, _) =>
        TimeSpan.FromSeconds(((RateLimitRejectedException)ex).RetryAfter.TotalSeconds + 0.1),
        (_, _, _, _) => Task.CompletedTask);
var wrap = Policy.WrapAsync(retry, rl);
await wrap.ExecuteAsync(ctx => doWork(ctx), new Context(), CancellationToken.None);
Defensive patterns

Strategy: fallback

Validate before calling

// Choose a strategy up front: either supply a retryAfterFactory or wrap in retry
var rl = Policy.RateLimitAsync<TResult>(10, TimeSpan.FromSeconds(1), 10,
    (retryAfter, ctx) => /* fallback value */ default);
await rl.ExecuteAsync(func, context, ct);

Try / catch

try { await rl.ExecuteAsync(func, context, ct); }
catch (RateLimitRejectedException ex) {
    await Task.Delay(ex.RetryAfter.Add(TimeSpan.FromMilliseconds(50)), ct);
    // retry or fall back
}

Prevention

When it happens

Trigger: An AsyncRateLimitPolicy (without retryAfterFactory) executes a delegate while the token bucket is exhausted; PermitExecution returns (false, retryAfter), retryAfterFactory is null, so the engine throws RateLimitRejectedException(retryAfter). Spikes/bursts that exceed the configured numberOfExecutions/perTimeSpan trigger it.

Common situations: Traffic exceeding the configured rate; maxBurst set too low for startup storms; perTimeSpan/numberOfExecutions mis-sized for real load; forgetting to add a retry policy around the rate-limited call; a retryAfterFactory intended to convert rejection to a fallback value was omitted.

Related errors


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