Back to blog

CancellationToken for Async Programming

July 28, 20237 min read

Asynchronous programming allows tasks to run without blocking the main thread, but managing these tasks efficiently is crucial. CancellationToken provides a robust mechanism for task cancellation, ensuring resources are not wasted and applications remain responsive.

Development Series — 23 articles
  1. Mastering Git Repository Organization
  2. CancellationToken for Async Programming
  3. Git Flow Rethink: When Process Stops Paying Rent
  4. Understanding System Cache: A Comprehensive Guide
  5. Guide to Redis Local Instance Setup
  6. Fire and Forget for Enhanced Performance
  7. Building Resilient .NET Applications with Polly
  8. The Singleton Advantage: Managing Configurations in .NET
  9. Troubleshooting and Rebuilding My JS-Dev-Env Project
  10. Decorator Design Pattern - Adding Telemetry to HttpClient
  11. Generate Wiki Documentation from Your Code Repository
  12. TaskListProcessor - Enterprise Async Orchestration for .NET
  13. Architecting Agentic Services in .NET 9: Semantic Kernel
  14. NuGet Packages: Benefits and Challenges
  15. My Journey as a NuGet Gallery Developer and Educator
  16. Harnessing the Power of Caching in ASP.NET
  17. The Building of React-native-web-start
  18. TailwindSpark: Ignite Your Web Development
  19. Creating a PHP Website with ChatGPT
  20. Evolving PHP Development
  21. Modernizing Client Libraries in a .NET 4.8 Framework Application
  22. Building Git Spark: My First npm Package Journey
  23. Dave's Top Ten: Git Stats You Should Never Track

CancellationToken for Efficient Asynchronous Programming

On a recent project, I watched a production background task queue grow unbounded because the team was passing CancellationToken without ever checking it. The service was consuming 90% CPU, churning through work that no caller was waiting for anymore. By the time we traced it down, the fix was embarrassingly simple — but the damage was real. That's the thing about CancellationToken: passing it into a method signature feels like the work is done. It isn't. The only reliable way I've found to actually stop async work in .NET is to pass a CancellationToken and listen for it. Everything else is wishful thinking.

What is a CancellationToken?

A CancellationToken is an object that allows you to communicate the cancellation status to an asynchronous operation. It signals to an operation that the caller is no longer interested in the result and the operation should stop. For example, if a user cancels a search in a web application, you can use a CancellationToken instance to tell the server to stop processing the request.

In practice, accepting a CancellationToken is just the first step. The real work is actually checking it inside your method.

One common entry point is the Task class in C#, which represents an asynchronous operation that can accept a CancellationToken. You pass the token to Task.Run or to the async method directly:

public async Task DownloadFileAsync(string url,
  string filename,
  HttpClient client,
  CancellationToken cancellationToken)
{
  await client.DownloadFileTaskAsync(url, filename, cancellationToken);
}

In this example, the DownloadFileAsync method takes a URL and a filename as input, along with a CancellationToken. It accepts an instance of the HttpClient class, which is used to download the file. The DownloadFileTaskAsync method is called on the client object, passing in the URL, filename, and cancellationToken. If the CancellationToken is canceled before the download is complete, the Task will be canceled.

Most asynchronous libraries accept a CancellationToken in their method signatures, which makes propagation straightforward — as long as you actually use it.

Checking For Cancellation

What I've found is that teams often skip cancellation altogether, and then wonder why their background services won't shut down cleanly. When you are writing asynchronous code that loops, polls, or processes a queue, you need to check the token actively — not just pass it along and hope something downstream handles it.

Key Considerations

When implementing cancellation checks in your code, consider the following:

  • Identify the points in your code where cancellation is allowed — not all operations can be safely interrupted
  • Determine how you want to handle cancellation at each point — should you throw an exception, return early, or clean up resources?
  • Consider the granularity of cancellation — too frequent checks can impact performance, too infrequent can make cancellation unresponsive
  • Place cancellation checks close to critical points — before expensive operations or at the start of loop iterations

You can call the ThrowIfCancellationRequested() method to check if the CancellationToken has been canceled and throw an OperationCanceledException if it has. Where in your business logic you place that call matters more than most developers realize.

Cancellation Example

public async Task DownloadFileAsync(List<string> urls,
  string filename,
  HttpClient client,
  CancellationToken cancellationToken)
{
  try 
  {
    foreach (var url in urls)
    {
      // Check for cancellation before each download
      cancellationToken.ThrowIfCancellationRequested();
      await client.DownloadFileTaskAsync(url, filename, cancellationToken);
    }
  }
  catch (TaskCanceledException ex)
  {
    // Handle cancellation thrown by client.DownloadFileTaskAsync() here
  }
  catch (OperationCanceledException ex)
  {
    // Handle cancellation thrown by ThrowIfCancellationRequested() here
  }
}

Where I've Seen This Go Wrong

I've watched cancellation handling fail in the same three ways repeatedly, across different teams and codebases.

The first is passing the token but never checking it in loops. This was the exact failure I described at the top. The token is threaded through the method signature, the caller cancels, and nothing stops. The loop keeps spinning because ThrowIfCancellationRequested() was never called inside it. Here's what that looks like versus the correct approach:

// Wrong: token is passed but never checked in the loop
foreach (var item in items)
{
    await ProcessItemAsync(item); // no token passed, no check
}

// Right: check before each unit of work
foreach (var item in items)
{
    cancellationToken.ThrowIfCancellationRequested();
    await ProcessItemAsync(item, cancellationToken);
}

The second pitfall is confusing cancellation with timeout. A CancellationTokenSource can be created with a timeout, which means it cancels automatically after a duration — but that is not the same as a simple delay or a Task.Delay. I've seen developers use CancellationToken where they actually wanted a retry policy, and the result was silent failures that looked like cancellations in logs.

// A timeout-based token — this cancels after 5 seconds
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await DoWorkAsync(cts.Token);

The third is calling async methods that don't accept a CancellationToken at all. If you're wrapping a third-party library that has no token parameter, your cancellation check has to happen before you call into it. Once you're inside that call, you can't interrupt it from outside.

Best Practices

Do This

  • Propagate CancellationToken to all methods — pass the token down through your call stack to enable cancellation at any level
  • Always check the token in loops — the most common place cancellation is ignored is inside a foreach or while, where the work actually lives
  • Handle cancellation exceptions properly — use try-catch blocks to catch OperationCanceledException and TaskCanceledException
  • Use IsCancellationRequested for non-throwing checks — when you need to check cancellation state without throwing an exception

Consider This

  • Don't cancel after point of no return — some operations cannot be safely canceled once started (e.g., database commits)
  • Balance granularity vs performance — too many cancellation checks can impact performance; find the right balance
  • Ensure proper resource cleanup — use try-finally or using statements to ensure resources are released even when canceled
  • Leave objects in valid state — when an operation is canceled, ensure that any partially modified objects are left in a valid, consistent state

Careful placement of ThrowIfCancellationRequested() in your business logic is what separates responsive async code from code that merely looks correct. Keep in mind that the method only checks if cancellation has been requested and throws OperationCanceledException if necessary — it does nothing automatically.

If you need to check the state of the cancellation token without throwing an exception, the IsCancellationRequested property returns a boolean indicating whether cancellation has been requested. This is useful when you want to branch behavior rather than exit immediately.

In long-running or expensive operations, also consider what happens to acquired resources and partial results when cancellation fires. The token won't clean those up for you.

Sample Code

For more complete coding examples of asynchronous techniques in .NET, check out the WebSpark repository on GitHub. This repository provides a wide range of examples of how to use CancellationToken and other asynchronous programming techniques in a safe and efficient manner.

Conclusion

What I've found working with cancellation in async code is that the pattern becomes second nature once you commit to it. Pass the CancellationToken through every async method in the chain, check it explicitly inside loops, handle the OperationCanceledException gracefully, and let the framework do what it was designed to do. The alternative — letting abandoned tasks consume resources quietly — is the kind of subtle problem that doesn't surface until production traffic reveals it. I've seen services brought to their knees by work that should have stopped the moment a caller disconnected.

With these patterns in place, async code becomes both more reliable and easier to reason about.

Explore More