.NET Academy

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 call Cancel().
  • 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 cancellation

Passing 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 TimeoutException

Combine 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 s

In 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

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • CancellationTokenSource cancels; the CancellationToken is 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) or CancelAfter.

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).

Program.cs
Loading editor…

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 pass
  1. 1.Who decides to cancel, and who reacts to it?

  2. 2.Does cancelling a token forcibly stop the running code?

  3. 3.What exception signals that an operation was cancelled?

  4. 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)