Module 1 · Runtime, Dependency Injection, and Async Foundations · Lesson 2 of 8
Async, Cancellation, and Timeout Budgets
The production question behind async/await
A senior interview rarely ends at “async avoids blocking threads.” The real question is whether work stops when the caller no longer needs it, whether time budgets are enforced consistently, and whether cancellation is distinguishable from failure.
Cancellation is cooperative
A CancellationToken is a signal, not a thread abort. Code must pass the token to operations that support it and check it in long CPU loops. Cancellation can race with successful completion, so design the operation to remain correct in either outcome.
Controller boundary
An ASP.NET Core action can accept a CancellationToken. The framework binds it to HttpContext.RequestAborted. Pass it through every layer instead of replacing it with CancellationToken.None.
public async Task<IResult> GetOrder(
Guid id,
CancellationToken requestAborted)
{
var order = await service.GetAsync(id, requestAborted);
return order is null ? Results.NotFound() : Results.Ok(order);
}A service method should normally accept the token as its final parameter. Repository calls must pass it to EF Core terminal operators such as SingleOrDefaultAsync, ToListAsync, and SaveChangesAsync. HTTP calls should pass it to SendAsync.
Deadlines and timeout budgets
A request often crosses several dependencies. Giving every dependency a separate 30-second timeout can turn a 30-second user budget into minutes. Start with one end-to-end budget and allocate smaller limits to downstream calls.
public async Task<Quote> GetQuoteAsync(
QuoteRequest request,
CancellationToken callerToken)
{
using var budget = CancellationTokenSource.CreateLinkedTokenSource(callerToken);
budget.CancelAfter(TimeSpan.FromSeconds(2));
return await pricingClient.GetAsync(request, budget.Token);
}The linked token cancels when either the caller leaves or the local budget expires. In a mature system, prefer a deadline value propagated between services because it preserves the remaining budget across hops. A token alone communicates cancellation but not the original deadline.
HttpClient timeouts
HttpClient.Timeout is a global upper bound for that client instance. Per-operation linked cancellation is more expressive because different operations can have different budgets. If both are configured, the shorter limit wins and the resulting exception can be hard to attribute. Standardize the policy, log the effective deadline, and avoid stacking unrelated timeout mechanisms.
Why Task.WhenAny is easy to get wrong
A common timeout implementation races the work against Task.Delay. If the delay wins but the original operation is not cancelled and observed, the work continues in the background, consumes resources, and may later fault without proper handling.
Incorrect idea:
var completed = await Task.WhenAny(work, Task.Delay(timeout)); if (completed != work) throw new TimeoutException();
Better: give work a cancellable token, cancel it when the budget expires, and await it so disposal and exceptions are observed. For APIs that cannot be cancelled, Task.WhenAny can limit how long the caller waits, but it cannot stop the underlying operation. State that limitation explicitly.
OperationCanceledException semantics
Do not log every OperationCanceledException as an application error. If callerToken.IsCancellationRequested, the client cancelled. If a local budget token fired while the caller token did not, it is a timeout. These are operationally different.
try
{
return await dependency.CallAsync(token);
}
catch (OperationCanceledException) when (callerToken.IsCancellationRequested)
{
logger.LogDebug("Request was cancelled by the caller");
throw;
}
catch (OperationCanceledException)
{
throw new TimeoutException("Pricing dependency exceeded its budget");
}Be careful when translating exceptions: preserve the original exception as InnerException and do not convert host shutdown cancellation into a retryable dependency timeout.
CPU-bound work
Async does not make CPU work free. A parsing or cryptographic loop must periodically call token.ThrowIfCancellationRequested(). The interval is a trade-off: checking every iteration may add overhead; checking too rarely makes shutdown and client cancellation sluggish.
for (var i = 0; i < rows.Count; i++)
{
if ((i & 1023) == 0)
token.ThrowIfCancellationRequested();
Process(rows[i]);
}Do not use Task.Run around arbitrary server work just to make a method appear asynchronous. ASP.NET Core already schedules requests on thread-pool threads. Task.Run can hide blocking I/O and makes capacity harder to reason about.
Background services and graceful shutdown
BackgroundService.ExecuteAsync receives stoppingToken. Pass it to queue reads, delays, network calls, and database operations. Decide what an in-flight message should do on shutdown: abandon safely for redelivery, checkpoint, or finish within a shutdown grace period. Never swallow the shutdown token and start unlimited non-cancellable work.
Cancellation and data consistency
A database cancellation request does not prove the server rolled back every side effect. Define transaction boundaries and idempotency separately. If a request sends a payment then gets cancelled before recording the result, a retry may duplicate the payment unless an idempotency key protects the external call.
Testing cancellation
- Use a controllable fake dependency that waits on the provided token.
- Cancel a
CancellationTokenSourceand assert the method completes promptly. - Assert the exact token reaches EF Core or HttpClient collaborators.
- Test caller cancellation separately from local timeout.
- Ensure cancelled work does not emit error-level logs.
- Verify resources and scopes are disposed after cancellation.
Interview scenario
An endpoint has a two-second SLA, calls inventory and pricing in parallel, then writes a quote. Explain the shared deadline, cancellation propagation to both calls, how you observe both tasks, what happens if one call fails, and why the final write needs an idempotent command. Mention telemetry fields such as remaining budget, dependency duration, cancellation source, and correlation ID.
Answer pattern
Describe the boundary token, the end-to-end deadline, propagation through every I/O call, exception classification, cleanup, and the consistency plan. The best answer shows that cancellation is a resource-control and correctness mechanism, not merely an optional async parameter.