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
- Wrap the rate-limited policy in a Retry policy with backoff that respects RetryAfter to ride out throttling.
- Provide a retryAfterFactory in RateLimitAsync to return a fallback TResult instead of throwing.
- Increase numberOfExecutions and/or maxBurst after measuring actual throughput needs.
- 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
- Always pair RateLimitAsync with a Retry policy that honors RetryAfter.
- Size numberOfExecutions and maxBurst from real throughput measurements.
- Consider a retryAfterFactory to convert throttling into a deliberate fallback value.
- Monitor RateLimitRejectedException frequency to tune the limit.
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
- The operation has been rate-limited and should be retried af
- Value cannot be null. (Parameter 'action')
- numberOfExecutions per timespan must be an integer greater t
- perTimeSpan must be a positive timespan.
- maxBurst must be an integer greater than or equal to 1.
AI-assisted analysis of App-vNext/Polly@d0e46bdb1e (2026-08-13).
Data as JSON: /api/errors/4e79862c7f108506.
Report an issue: GitHub.