← Back to all posts

CancellationTokens in .NET

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 CancellationToken in 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 CancellationTokenSource everywhere without linking
  • passing CancellationToken.None in data/network calls
  • catching Exception and 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