App-vNext/Polly · error · TimeoutRejectedException
The delegate executed asynchronously through TimeoutPolicy d
Error message
The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.
What it means
TimeoutRejectedException is the runtime signal that the executed delegate did not finish within the configured timeout. AsyncTimeoutEngine throws it (AsyncTimeoutEngine.cs:49) only when an OperationCanceledException coincides with the internal timeout token being cancelled — i.e. the policy's own timeout fired, not the caller's cancellation. The wrapped exception is the original OperationCanceledException and the Timeout property carries the configured TimeSpan.
Source
Thrown at src/Polly/Timeout/AsyncTimeoutEngine.cs:49
// else: timeoutStrategy == TimeoutStrategy.Pessimistic
Task<TResult> timeoutTask = timeoutCancellationTokenSource.Token.AsTask<TResult>();
SystemClock.CancelTokenAfter(timeoutCancellationTokenSource, timeout);
actionTask = action(context, combinedToken);
return await (await Task.WhenAny(actionTask, timeoutTask).ConfigureAwait(continueOnCapturedContext)).ConfigureAwait(continueOnCapturedContext);
}
catch (Exception ex)
{
// Note that we cannot rely on testing (operationCanceledException.CancellationToken == combinedToken || operationCanceledException.CancellationToken == timeoutCancellationTokenSource.Token)
// as either of those tokens could have been onward combined with another token by executed code, and so may not be the token expressed on operationCanceledException.CancellationToken.
if (ex is OperationCanceledException && timeoutCancellationTokenSource.IsCancellationRequested)
{
await onTimeoutAsync(context, timeout, actionTask, ex).ConfigureAwait(continueOnCapturedContext);
throw new TimeoutRejectedException("The delegate executed asynchronously through TimeoutPolicy did not complete within the timeout.", timeout, ex);
}
throw;
}
finally
{
// If timeoutCancellationTokenSource was canceled & our combined token hasn't been signaled, cancel it.
// This avoids the exception propagating before the linked token can signal the downstream to cancel.
// See https://github.com/App-vNext/Polly/issues/722.
// stryker disable once all : no means to test this
if (!combinedTokenSource.IsCancellationRequested && timeoutCancellationTokenSource.IsCancellationRequested)
{
#if NET8_0_OR_GREATER
await combinedTokenSource.CancelAsync().ConfigureAwait(false);
#else
combinedTokenSource.Cancel();
#endif
}View on GitHub (pinned to d0e46bdb1e)
Solutions
- Catch TimeoutRejectedException specifically and apply your fallback (cached value, default, or rethrow to a higher-level handler).
- Increase the configured timeout to a realistic value for the operation's p99 latency.
- If using optimistic timeout, ensure the executed delegate observes and responds to the CancellationToken it receives.
- For stubborn non-cancellable calls, switch to TimeoutStrategy.Pessimistic so the policy does not rely on cooperative cancellation.
- Wrap the timeout policy in a retry or fallback policy to recover gracefully.
Example fix
// before
var policy = Policy.TimeoutAsync(2, TimeoutStrategy.Optimistic);
await policy.ExecuteAsync(ct => httpClient.GetStringAsync(url), CancellationToken.None);
// throws TimeoutRejectedException when the call exceeds 2s
// after
var timeout = Policy.TimeoutAsync(10, TimeoutStrategy.Pessimistic);
var fallback = Policy<object>
.Handle<TimeoutRejectedException>()
.FallbackAsync(_ => Task.FromResult<object>(defaultValue));
var wrapped = fallback.WrapAsync(timeout);
await wrapped.ExecuteAsync(ct => httpClient.GetStringAsync(url, ct), CancellationToken.None); Defensive patterns
Strategy: try-catch
Type guard
static bool IsTimeoutRejected(Exception ex) => ex is TimeoutRejectedException;
Try / catch
try
{
await timeoutPolicy.ExecuteAsync(ct => DoWorkAsync(ct), CancellationToken.None);
}
catch (TimeoutRejectedException ex)
{
logger.LogWarning(ex, "Operation timed out after {Timeout}", ex.Timeout);
return fallbackValue; // or rethrow to a higher-level handler
} Prevention
- Tune the timeout to the operation's realistic p99 latency plus headroom; a too-small timeout causes chronic TimeoutRejectedException.
- For optimistic timeout, ensure the executed delegate honours the CancellationToken it receives so cancellation is cooperative.
- Wrap the timeout policy in a fallback or retry policy so a timeout degrades gracefully instead of surfacing as an error.
- Use TimeoutStrategy.Pessimistic for non-cancellable work so the policy is not dependent on cooperative cancellation.
- Log ex.Timeout from the exception to confirm the configured deadline vs the actual failure.
When it happens
Trigger: Executing a delegate through an AsyncTimeoutPolicy whose action takes longer than the configured timeout (optimistic: the action ignores the CancellationToken; pessimistic: the action runs past the deadline). The engine detects timeoutCancellationTokenSource.IsCancellationRequested inside the OperationCanceledException handler and converts it to TimeoutRejectedException.
Common situations: An HTTP call or DB query that stalls and exceeds the timeout; an optimistic timeout where the downstream code does not honour the CancellationToken; a timeout value set too low for normal latency; network degradation or a slow dependency under load; deadlocks in the executed delegate.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- The delegate executed through TimeoutPolicy did not complete
- Invalid delay specified.
- onRetryAsync
- action
- seconds
AI-assisted analysis of App-vNext/Polly@d0e46bdb1e (2026-08-13).
Data as JSON: /api/errors/0686b2807ddea0c2.
Report an issue: GitHub.