Mastering Cancellation Tokens in .NET: The Complete Guide
Bhrugen Patel
Author
RESTful Web API in .NET Core - The Advanced Guide (.NET 10)
Intermediate course on RESTful API with ASP.NET Core Web API that will dive into concepts like file uploads, refresh token, exception handling and more!
CancellationToken in ASP.NET Core APIs: Stop Wasting Server Resources
Here's a scenario: A user makes a request to your API, it starts processing, then the user gets impatient and closes their browser. Your API? Still chugging away, doing work that nobody will ever see. Database queries running, external APIs being called, all for nothing.
That's where CancellationToken comes in. It's your way of saying ""hey, if the client doesn't care anymore, let's stop wasting resources.""
What Even Is a CancellationToken?
Think of CancellationToken as a messenger that tells your code ""the client disconnected"" or ""this operation is taking too long, give up."" When that happens, you can bail out early instead of finishing work nobody wants.
💡 The Magic Part
ASP.NET Core automatically creates and passes a CancellationToken to your controller actions. When the HTTP request is aborted (user closes browser, timeout, etc.), that token gets cancelled. You just need to use it!
The Simplest Example
Here's what most people do (the wrong way):
[HttpGet(""products"")]
public async Task<IActionResult> GetProducts()
{
// This will keep running even if the client disconnects
var products = await _context.Products.ToListAsync();
return Ok(products);
}
Now here's the better way:
[HttpGet(""products"")]
public async Task<IActionResult> GetProducts(CancellationToken cancellationToken)
{
// If the client disconnects, this stops automatically
var products = await _context.Products.ToListAsync(cancellationToken);
return Ok(products);
}
That's it! Just add the parameter and pass it along. ASP.NET Core handles the rest.
A Real-World Example: Report Generation
Let's say you're building a report generation endpoint. It takes 5 seconds to generate a sales report. A user clicks ""Generate Report"", waits 2 seconds, gets impatient, and closes the browser tab.
❌ Without CancellationToken (The Problem)
[HttpGet(""reports/sales"")]
public async Task<IActionResult> GenerateSalesReport()
{
// Simulate fetching data from database (2 seconds)
await Task.Delay(2000);
var salesData = await _context.Sales.ToListAsync();
// Simulate processing and generating report (3 seconds)
await Task.Delay(3000);
var report = GenerateReport(salesData);
return Ok(report);
}
🔥 The Problem
- • User closes browser after 2 seconds
- • Your server keeps running for another 3 seconds
- • Database connections stay open unnecessarily
- • Server resources wasted on work nobody will see
- • Multiply this by 100 impatient users = serious resource drain!
✅ With CancellationToken (The Solution)
[HttpGet(""reports/sales"")]
public async Task<IActionResult> GenerateSalesReport(CancellationToken cancellationToken)
{
try
{
// Simulate fetching data from database (2 seconds)
await Task.Delay(2000, cancellationToken);
var salesData = await _context.Sales.ToListAsync(cancellationToken);
// Simulate processing and generating report (3 seconds)
await Task.Delay(3000, cancellationToken);
var report = GenerateReport(salesData);
return Ok(report);
}
catch (OperationCanceledException)
{
// Client disconnected - log it and move on
_logger.LogInformation(""Report generation cancelled by client"");
return StatusCode(499, ""Client closed request"");
}
}
✨ What Changed?
- • User closes browser after 2 seconds
- • CancellationToken gets triggered immediately
- • Task.Delay throws OperationCanceledException
- • Processing stops instantly - no wasted 3 seconds!
- • Database connections released immediately
- • Server resources freed up for actual users
💡 Real Impact
Imagine this scenario at scale:
Without CancellationToken:
100 impatient users × 5 seconds wasted = 500 seconds of wasted server time
With CancellationToken:
Operations stop immediately when cancelled = Minimal resource waste
Passing It Through Your Service Layer
Don't just use it in controllers! Pass it through your entire call chain:
// Controller
[HttpGet(""orders/{id}"")]
public async Task<IActionResult> GetOrder(
int id,
CancellationToken cancellationToken)
{
var order = await _orderService.GetOrderDetailsAsync(id, cancellationToken);
return Ok(order);
}
// Service
public class OrderService
{
public async Task<OrderDto> GetOrderDetailsAsync(
int orderId,
CancellationToken cancellationToken)
{
var order = await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
// Call another service
var shipping = await _shippingService
.GetShippingInfoAsync(order.ShippingId, cancellationToken);
return MapToDto(order, shipping);
}
}
// External API call
public class ShippingService
{
private readonly HttpClient _httpClient;
public async Task<ShippingInfo> GetShippingInfoAsync(
int shippingId,
CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync(
$""https://api.shipping.com/info/{shippingId}"",
cancellationToken); // HttpClient supports it too!
return await response.Content
.ReadFromJsonAsync<ShippingInfo>(cancellationToken);
}
}
💡 Notice how the CancellationToken flows through every layer? That's the pattern. Don't stop at the controller!
Best Practices
1. Always add CancellationToken to async methods
Make it the last parameter. It's optional by default (uses CancellationToken.None).
2. Pass it through your entire call stack
Controller → Service → Repository → Database. Every layer should respect cancellation.
3. Don't swallow OperationCanceledException
Either let it bubble up or handle it specifically. Don't catch it as a generic Exception and ignore it.
4. Check cancellation in long-running loops
If you're processing thousands of items, check every N iterations at minimum.
5. Remember HttpClient already supports it
Most .NET libraries do! Always look for overloads that accept CancellationToken.
When NOT to Use CancellationToken
🚫 Don't use it for:
- • Critical operations that MUST complete (payments, data commits)
- • Fire-and-forget background tasks that run independently
- • Operations where partial completion is dangerous (like partial database writes)
If cancelling an operation would leave your system in an inconsistent state, you probably shouldn't use CancellationToken there!
The Bottom Line
CancellationToken is one of those features that seems like extra work at first, but it's actually super easy once you get into the habit. Just add it as a parameter and pass it along. Your server will thank you for not wasting resources on abandoned requests.
⚡ Quick Recap:
- Add
CancellationTokenas the last parameter in your API methods - ASP.NET Core automatically cancels it when the client disconnects
- Pass it through your entire call stack (controllers → services → repositories)
- Most .NET async methods support it out of the box
- Catch
OperationCanceledExceptionto handle cancellation gracefully - Don't use it for critical operations that must complete
Start adding it today, your API will be more efficient and your server less stressed!