Cancellation and timeouts
Stop work that's no longer needed with CancellationToken, and put limits on slow operations.
11 min read
You will learn
- Create and cancel a CancellationTokenSource
- Pass a CancellationToken through async calls
- Handle OperationCanceledException
- Implement timeouts with CancelAfter
Stop work nobody needs
A user closes the browser tab while the server is still building a 30-second report. A search is superseded by the next keystroke. A dependency hangs and never answers. In each case, continuing the work wastes CPU, database connections and threads. .NET's answer is cooperative cancellation.
The two halves
CancellationTokenSource— the remote control. Whoever owns it can callCancel().CancellationToken— a read-only signal handed to the work. Code can check it, but can't cancel it.
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task work = ProcessAsync(token); // the work receives the token
cts.Cancel(); // later: request cancellationPassing tokens through
By convention, async methods take an optional CancellationToken as their last parameter and pass it on to everything they call:
async Task<Report> BuildReportAsync(int year, CancellationToken cancellationToken = default)
{
var orders = await db.Orders
.Where(o => o.Year == year)
.ToListAsync(cancellationToken); // EF Core observes it
var rates = await http.GetFromJsonAsync<Rates>(ratesUrl, cancellationToken); // so does HttpClient
return Summarize(orders, rates);
}When the token is cancelled, these calls throw OperationCanceledException (often its subclass TaskCanceledException), which unwinds your method.
Checking in your own loops
Cancellation is cooperative — nothing is killed. CPU-bound loops must check:
foreach (var file in files)
{
cancellationToken.ThrowIfCancellationRequested(); // throws if cancelled
Process(file);
}Timeouts
A timeout is just cancellation scheduled in advance:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // or cts.CancelAfter(...)
try
{
var price = await GetPriceAsync(productId, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("The price service took too long");
}
// Or, for any task:
var result = await SomeTaskAsync().WaitAsync(TimeSpan.FromSeconds(5)); // throws TimeoutExceptionCombine a caller's token with your own timeout using a linked source:
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linked.CancelAfter(TimeSpan.FromSeconds(10));
await CallServiceAsync(linked.Token); // cancelled by the caller OR after 10 sIn ASP.NET Core
Add a CancellationToken parameter to an endpoint and ASP.NET Core fills it with a token that's cancelled when the client disconnects:
app.MapGet("/reports/{year}", async (int year, ReportService reports, CancellationToken ct) =>
await reports.BuildReportAsync(year, ct));Tip
Treat OperationCanceledException as a normal outcome, not an error: don't log it as a failure, and don't catch it only to swallow it deep inside library code — let it propagate to whoever asked for the cancellation.
Try it
C# runtime not loaded
Key takeaways
CancellationTokenSourcecancels; theCancellationTokenis passed to the work.- Cancellation is cooperative: pass tokens down and check them in long loops.
- Cancelled operations throw
OperationCanceledException. - Timeouts are scheduled cancellation:
new CancellationTokenSource(timeout)orCancelAfter.
Exercise
SlowWorkAsync is provided. Implement DownloadWithTimeoutAsync, which returns the work's result, or "timed out" if it takes longer than timeoutMs. Then implement CountUntilCancelledAsync, which counts up every 10 ms until the token is cancelled, then returns the count (without throwing).
C# runtime not loaded
Hint 1
using var cts = new CancellationTokenSource(); cts.CancelAfter(timeoutMs); then pass cts.Token to SlowWorkAsync.
Hint 2
Wrap the await in try { ... } catch (OperationCanceledException) { return "timed out"; } — TaskCanceledException derives from it.
Hint 3
CountUntilCancelledAsync: while (true) { await Task.Delay(10, token); count++; } inside a try, and return count from the catch.
Show solution
Console.WriteLine(await DownloadWithTimeoutAsync(workMs: 50, timeoutMs: 500));
async Task<string> SlowWorkAsync(int ms, CancellationToken token)
{
await Task.Delay(ms, token); // throws if the token is cancelled
return "done";
}
async Task<string> DownloadWithTimeoutAsync(int workMs, int timeoutMs)
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(timeoutMs);
try
{
return await SlowWorkAsync(workMs, cts.Token);
}
catch (OperationCanceledException)
{
return "timed out";
}
}
async Task<int> CountUntilCancelledAsync(CancellationToken token)
{
int count = 0;
try
{
while (true)
{
await Task.Delay(10, token);
count++;
}
}
catch (OperationCanceledException)
{
return count;
}
}Quiz
4 questions · 3 correct to pass1.Who decides to cancel, and who reacts to it?
2.Does cancelling a token forcibly stop the running code?
3.What exception signals that an operation was cancelled?
4.In ASP.NET Core, where does a useful CancellationToken come from?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)