Case 15: The Resource Saver - Using CancellationToken Correctly
The Story
Imagine your ASP.NET Core API has an endpoint to generate a complex report. The database query for this report can take 10-30 seconds to complete.
An impatient user clicks the button to request the report, but after waiting for only 5 seconds, they close the browser tab or navigate to another page.
The Hidden Problem: Even though the user is gone, on the server, the database query continues to run until it's finished. The server is wasting CPU, RAM, and a database connection to do work for a user who will never see the result.
The Problem Code (Ignoring CancellationToken)
// Controller
[HttpGet("generate-report")]
public async Task<IActionResult> GenerateReport()
{
// Calling the service without passing a token
var reportData = await _reportService.GenerateReportAsync();
return Ok(reportData);
}
// Service
public async Task<ReportData> GenerateReportAsync() // <-- Missing CancellationToken
{
// This query takes a long time
var result = await _context.Orders
.Where(...) // Complex filtering
.GroupBy(...)
.ToListAsync(); // <-- Not passing a CancellationToken
// ...
return reportData;
}Why is this a problem?
Wasted Resources: When a user closes their browser tab, ASP.NET Core knows the connection has been aborted. It triggers a
CancellationTokenfor that specific request."Zombie" Work: Because our code doesn't accept or pass this
CancellationTokendown to the lower layers (especially to EF Core methods like.ToListAsync()), the query continues to run blindly.The Result: Under high load, many of these "zombie" queries can build up. They can use up all the server's resources (especially the database connection pool), slowing down the application for active users and potentially causing it to hang.
The Solution: Pass the CancellationToken Through All Layers
A CancellationToken is a signal that allows different parts of your application to cooperatively cancel an operation. ASP.NET Core automatically gives you a token for each HTTP request. Your job is to pass it along.
Here is the better code:
// Controller
[HttpGet("generate-report")]
// ASP.NET Core will automatically provide this CancellationToken
public async Task<IActionResult> GenerateReport(CancellationToken cancellationToken)
{
try
{
var reportData = await _reportService.GenerateReportAsync(cancellationToken);
return Ok(reportData);
}
catch (OperationCanceledException)
{
// Catch this exception when the query is canceled
_logger.LogInformation("Client cancelled the report generation request.");
// Do nothing or return a specific status code
return StatusCode(499); // Client Closed Request
}
}
// Service
public async Task<ReportData> GenerateReportAsync(CancellationToken cancellationToken)
{
var result = await _context.Orders
.Where(...)
.GroupBy(...)
// Pass the token to the EF Core method
.ToListAsync(cancellationToken);
// ...
return reportData;
}What We Gained
- Resource Savings: As soon as the user closes the tab, the
cancellationTokenis triggered. This signal is passed to.ToListAsync(). EF Core immediately sends a cancellation command to the database. The database stops the long-running query. - Immediate Resource Release: The CPU, RAM, and database connection being used by that query are freed up instantly, ready to serve other requests.
- A More Stable Application: The system becomes more resilient to user behavior and can recover better under high load.
The Golden Rule
Always accept and pass a CancellationToken through your chain of asynchronous calls, especially for operations that could take a long time.
Most async methods in modern .NET libraries (like EF Core and HttpClient) have an overload that accepts a CancellationToken. Use them. Ignoring cancellation tokens means you are wasting server resources on work that is no longer needed.