CancellationTokens in .NET
Reading Time: 3 minutes
Problem
A lot of .NET applications accept a CancellationToken but do not propagate it through the full call chain.
Typical issues:
- HTTP request is canceled, but downstream work continues
- database queries still run after client disconnects
- background loops ignore shutdown signals and delay app stop
- threads and I/O stay busy with work nobody needs anymore
The result is wasted compute, slower recovery under load, and poor operational behavior.
Solution
Treat cancellation as part of your API contract and flow it end-to-end.
Core practices:
- accept
CancellationTokenin async public methods - pass the token to every cancellable dependency call
- honor cancellation quickly in loops and long-running workflows
- avoid swallowing
OperationCanceledException - use linked tokens when combining timeout + external cancellation
In ASP.NET Core, HttpContext.RequestAborted should usually be the root token for request work.
Description
1) Controller to service to repository propagation (.NET)
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _service;
public OrdersController(IOrderService service)
{
_service = service;
}
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> Get(int id, CancellationToken cancellationToken)
{
var order = await _service.GetByIdAsync(id, cancellationToken);
return order is null ? NotFound() : Ok(order);
}
}
public interface IOrderService
{
Task<OrderDto?> GetByIdAsync(int id, CancellationToken cancellationToken);
}
public class OrderService : IOrderService
{
private readonly AppDbContext _db;
public OrderService(AppDbContext db)
{
_db = db;
}
public async Task<OrderDto?> GetByIdAsync(int id, CancellationToken cancellationToken)
{
return await _db.Orders
.AsNoTracking()
.Where(o => o.Id == id)
.Select(o => new OrderDto(o.Id, o.Number, o.Total))
.FirstOrDefaultAsync(cancellationToken);
}
}
The key is not only accepting the token, but passing it to EF Core methods like FirstOrDefaultAsync.
2) Respect cancellation in loops
public async Task ProcessQueueAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var message = await _queue.ReceiveAsync(cancellationToken);
await HandleMessageAsync(message, cancellationToken);
}
}
For CPU-heavy loops, check periodically and stop early.
3) Linked token with timeout
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
var response = await _httpClient.GetAsync(url, linkedCts.Token);
This lets your operation cancel on either caller cancellation or timeout.
4) Handle cancellation correctly
try
{
await _worker.RunAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Operation canceled by request/shutdown.");
throw;
}
Do not convert cancellation into generic errors unless you intentionally map behavior (for example, request-aborted semantics).
5) Common mistakes
- creating new
CancellationTokenSourceeverywhere without linking - passing
CancellationToken.Nonein data/network calls - catching
Exceptionand hiding cancellation - doing long sync work inside async methods where cancellation cannot interrupt
Summary
CancellationTokens are not optional plumbing; they are a core resilience and efficiency mechanism in modern .NET apps.
When you propagate tokens from request entry points to I/O boundaries, your system:
- frees resources sooner
- behaves better under load
- shuts down more gracefully
- avoids unnecessary background and database work
The rule of thumb is simple: if a method can be canceled, accept a token and pass it through.
References
- Microsoft Docs: Cancellation in Managed Threads
- Microsoft Docs: Task Cancellation
- Microsoft Docs: HttpContext.RequestAborted
- Microsoft Docs: EF Core Async Query APIs