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

  1. Catch TimeoutRejectedException specifically and apply your fallback (cached value, default, or rethrow to a higher-level handler).
  2. Increase the configured timeout to a realistic value for the operation's p99 latency.
  3. If using optimistic timeout, ensure the executed delegate observes and responds to the CancellationToken it receives.
  4. For stubborn non-cancellable calls, switch to TimeoutStrategy.Pessimistic so the policy does not rely on cooperative cancellation.
  5. 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

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

Related errors


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