Cancellation is one of those features that is easy to ignore until it matters. When a user navigates away, a request times out, or a service shuts down, your code needs to stop doing unnecessary work. CancellationToken is the cooperative cancellation mechanism in .NET, and using it correctly makes your applications more responsive and resource-efficient.
The Basics
A CancellationTokenSource creates tokens. Code receiving the token periodically checks whether cancellation has been requested:
var cts = new CancellationTokenSource();
// Pass the token to async work
await DoWorkAsync(cts.Token);
// Cancel from another thread or after a timeout
cts.Cancel();
The word "cooperative" is key. Cancellation does not forcibly abort a thread. The code doing the work must check the token and decide to stop.
Always Pass Tokens Down
The most common mistake is accepting a CancellationToken parameter but not passing it to the methods you call:
// Bad — ignores cancellation in the HTTP call
public async Task<Order> GetOrderAsync(int id, CancellationToken ct)
{
var response = await _httpClient.GetAsync($"/orders/{id}"); // No token!
return await response.Content.ReadFromJsonAsync<Order>();
}
// Good — propagates cancellation to every async operation
public async Task<Order> GetOrderAsync(int id, CancellationToken ct)
{
var response = await _httpClient.GetAsync($"/orders/{id}", ct);
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
Every async API in .NET accepts a CancellationToken. Pass it through.
Linking Tokens
Sometimes you need to combine multiple cancellation sources — for example, a request-scoped token and a timeout:
public async Task<Report> GenerateReportAsync(CancellationToken requestToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
requestToken, timeoutCts.Token);
return await BuildReportAsync(linkedCts.Token);
}
The linked token fires if either the request is cancelled or the timeout expires. Always dispose linked token sources to avoid memory leaks.
Handling OperationCanceledException
When a token is cancelled, most framework methods throw OperationCanceledException (or its subclass TaskCanceledException). Handle it appropriately:
public async Task<IActionResult> GetData(CancellationToken ct)
{
try
{
var data = await _service.FetchDataAsync(ct);
return Ok(data);
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// Client disconnected — log and return gracefully
_logger.LogInformation("Request cancelled by client");
return StatusCode(499); // Client Closed Request
}
}
The when filter is important. It distinguishes between "our caller cancelled" and "something else threw an OperationCanceledException for an unrelated reason."
Timeouts with CancellationTokenSource
The constructor overload that accepts a TimeSpan is the cleanest way to implement timeouts:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
await LongRunningOperationAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new TimeoutException("Operation did not complete within 10 seconds");
}
Checking the Token in CPU-Bound Loops
For CPU-bound work, call ThrowIfCancellationRequested periodically:
public void ProcessLargeDataSet(IList<Record> records, CancellationToken ct)
{
for (int i = 0; i < records.Count; i++)
{
ct.ThrowIfCancellationRequested();
ProcessRecord(records[i]);
}
}
For very tight loops, check every N iterations to avoid the overhead of checking on every cycle:
for (int i = 0; i < records.Count; i++)
{
if (i % 1000 == 0)
ct.ThrowIfCancellationRequested();
ProcessRecord(records[i]);
}
Registering Callbacks
You can register a callback that fires when cancellation is requested. This is useful for cleaning up unmanaged resources or cancelling non-token-aware APIs:
public async Task StreamDataAsync(Stream stream, CancellationToken ct)
{
ct.Register(() => stream.Close()); // Force-close the stream on cancellation
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
{
await ProcessChunkAsync(buffer.AsMemory(0, bytesRead));
}
}
ASP.NET Core and HttpContext.RequestAborted
ASP.NET Core automatically provides a CancellationToken through HttpContext.RequestAborted. When using [FromServices] or parameter binding, you can accept it directly in controller actions:
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id, CancellationToken cancellationToken)
{
// This token fires when the client disconnects
var result = await _repository.GetByIdAsync(id, cancellationToken);
return Ok(result);
}
This is automatic model binding — ASP.NET Core sees the CancellationToken parameter and binds HttpContext.RequestAborted to it. No configuration needed.
Dispose Your CancellationTokenSources
CancellationTokenSource implements IDisposable. If you create one, dispose it:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await DoWorkAsync(cts.Token);
Failing to dispose can leak timer resources (when using the timeout constructor) and registered callbacks. The using declaration makes this effortless.