How to Use CancellationToken in ASP.NET Core Without Leaving Work Running?
An ASP.NET Core API may receive a request that takes several seconds to complete. During that time, the application might query a database, call an external API, process a file, or perform multiple asynchronous operations.
But what happens if the client disconnects before the request finishes?
Without proper cancellation handling, some operations may continue running even though nobody is waiting for the result. This can waste database connections, consume CPU time, and put unnecessary pressure on external services.
C# provides CancellationToken to let one part of an application signal that an operation should stop. ASP.NET Core also provides a request cancellation token that is triggered when the server detects that the client has disconnected or the request has been aborted.
The important point is that cancellation is cooperative. Passing a token to a method does not automatically stop every operation. The code and the underlying libraries must observe the token and respond to cancellation.
In this article, we will learn how to use CancellationToken correctly in ASP.NET Core, pass it through database and HTTP operations, handle cancellation safely, and avoid common mistakes.
What Is CancellationToken in C#?
CancellationToken is a .NET type that allows code to observe a cancellation request.
A CancellationTokenSource creates and controls a cancellation request. The token can then be passed to methods that support cancellation.
Consider this simple example:
using var cancellationTokenSource =
new CancellationTokenSource();
CancellationToken token =
cancellationTokenSource.Token;
cancellationTokenSource.Cancel();
Console.WriteLine(token.IsCancellationRequested);
The output is True because cancellation has been requested.
However, calling Cancel() does not forcibly terminate a running method. The method must check the token or call another operation that observes it.
For example:
public async Task ProcessAsync(
CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(
TimeSpan.FromSeconds(1),
cancellationToken);
}
}
The method checks for cancellation before each iteration. The delay also receives the token, so it can stop waiting when cancellation is requested.
If the operation is cancelled, it normally throws OperationCanceledException or a derived exception such as TaskCanceledException.
How ASP.NET Core Detects Request Cancellation
ASP.NET Core exposes HttpContext.RequestAborted, which signals that the request has been aborted.
In controller actions and minimal APIs, you can receive the request’s cancellation token as a method parameter.
For example:
[HttpGet("{id:int}")]
public async Task<IActionResult> GetOrder(
int id,
CancellationToken cancellationToken)
{
var order = await _dbContext.Orders
.AsNoTracking()
.FirstOrDefaultAsync(
order => order.Id == id,
cancellationToken);
if (order is null)
{
return NotFound();
}
return Ok(order);
}
ASP.NET Core supplies the request cancellation token to the action parameter.
When the request is aborted, the token signals cancellation. The database provider can then attempt to cancel the query if it supports cancellation.
This does not guarantee that every database command stops immediately. Cancellation behavior depends on the provider, the database server, and the operation already in progress.
It is also important to understand that a request may be aborted for reasons other than a user deliberately closing a browser tab. Network failures, client timeouts, and other interruptions can also lead to cancellation.
Step 1: Pass CancellationToken Through Every Layer
A common mistake is accepting a cancellation token in the controller but failing to pass it to the service or repository.
Consider the following example:
[HttpGet("report")]
public async Task<IActionResult> GetReport(
CancellationToken cancellationToken)
{
var report = await _reportService.GenerateAsync();
return Ok(report);
}
The controller receives a cancellation token, but the service cannot observe it because the token is not passed to GenerateAsync().
A better implementation passes the token through each layer.
[HttpGet("report")]
public async Task<IActionResult> GetReport(
CancellationToken cancellationToken)
{
var report = await _reportService.GenerateAsync(
cancellationToken);
return Ok(report);
}
The service can then pass the token to its database and HTTP operations.
public async Task<ReportResponse> GenerateAsync(
CancellationToken cancellationToken)
{
var orders = await _dbContext.Orders
.AsNoTracking()
.Where(order => order.IsCompleted)
.ToListAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
return new ReportResponse(
orders.Count,
orders.Sum(order => order.Total));
}
This example demonstrates token propagation. For a very large report, loading every matching order into a list may still consume substantial memory. Consider an aggregate database query or a streaming approach if the report does not need every order in memory.
Passing the token through the call chain gives each layer an opportunity to stop unnecessary work.
Step 2: Pass CancellationToken to Entity Framework Core
Entity Framework Core provides asynchronous methods that accept a cancellation token.
- Common examples include:
- ToListAsync(cancellationToken)
- FirstOrDefaultAsync(cancellationToken)
- SingleOrDefaultAsync(cancellationToken)
- SaveChangesAsync(cancellationToken)
- AnyAsync(cancellationToken)
For example, an update operation can pass the token to both the query and the database save.
public async Task<bool> UpdateOrderStatusAsync(
int orderId,
string status,
CancellationToken cancellationToken)
{
var order = await _dbContext.Orders
.FirstOrDefaultAsync(
order => order.Id == orderId,
cancellationToken);
if (order is null)
{
return false;
}
order.Status = status;
await _dbContext.SaveChangesAsync(
cancellationToken);
return true;
}
The token allows supported database operations to respond to cancellation.
However, cancellation is not a database transaction rollback guarantee. If a cancellation request arrives while a database operation is being processed, the application may not always know whether the operation completed before cancellation took effect.
For important writes, use appropriate transaction boundaries, idempotency, and database consistency rules. Do not assume that a cancelled HTTP request means the database change definitely did not happen.
Step 3: Cancel External HTTP Requests
ASP.NET Core applications frequently call other services through HttpClient. These calls can remain in progress after the original request has been aborted unless cancellation is propagated.
For example:
public class InventoryService
{
private readonly HttpClient _httpClient;
public InventoryService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetInventoryAsync(
int productId,
CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(
$"inventory/{productId}",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(
cancellationToken);
}
}
The token is passed to both the HTTP request and the response-content read.
This lets the application attempt to cancel the outgoing request when the original request is aborted. The downstream service may still have started or completed its own work, so cancellation does not guarantee that the remote operation is undone.
For production applications, also configure appropriate timeouts and handle transient failures separately from cancellation.
Step 4: Use a Timeout Without Losing Request Cancellation
Sometimes an operation should stop after a defined amount of time even when the client remains connected.
For example, a report query might have a maximum execution window of ten seconds.
You can combine a timeout token with the request cancellation token using a linked cancellation token source.
public async Task<ReportResponse> GenerateReportAsync(
CancellationToken requestCancellationToken)
{
using var timeoutSource =
new CancellationTokenSource(
TimeSpan.FromSeconds(10));
using var linkedSource =
CancellationTokenSource.CreateLinkedTokenSource(
requestCancellationToken,
timeoutSource.Token);
return await GenerateReportCoreAsync(
linkedSource.Token);
}
GenerateReportCoreAsync() represents the application’s report-generation method and must pass the token to its underlying operations.
The linked token is cancelled if either the request is aborted or the ten-second timeout expires.
If the application needs to distinguish these situations, inspect the original request token and timeout token when handling the cancellation exception. Do not report every cancellation as a client disconnect.
Also, avoid creating multiple competing timeout mechanisms without a clear policy. Configure HTTP client timeouts, database command timeouts, and application-level deadlines deliberately.
Step 5: Handle Cancellation Without Hiding It
Cancellation is a normal part of asynchronous application behavior. It should not automatically be logged as an application error.
For example:
try
{
await _reportService.GenerateAsync(
cancellationToken);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
// The request was cancelled.
throw;
}
This example rethrows cancellation instead of converting it into a successful response.
In many ASP.NET Core actions, explicit cancellation handling is unnecessary because the exception can propagate through the request pipeline. Add a catch block when the application needs specific cleanup or logging behavior.
Be careful not to catch OperationCanceledException and continue processing. Doing so can leave unnecessary work running after cancellation has been requested.
Also, do not attempt to write a normal error response after the client has disconnected. The response may no longer be writable.
Step 6: Stop Long-Running Loops and Background Work
Long-running CPU-bound operations need explicit cancellation checks because they may not call asynchronous methods that observe a token.
For example:
public Result CalculateReport(
IEnumerable<Order> orders,
CancellationToken cancellationToken)
{
decimal total = 0;
foreach (var order in orders)
{
cancellationToken.ThrowIfCancellationRequested();
total += order.Total;
}
return new Result(total);
}
public record Result(decimal Total);
The loop checks the token before processing each item.
For expensive calculations, choose a suitable cancellation-check frequency. Checking on every iteration may be appropriate for many loops, while a very tight computational loop may need a different balance.
There is another important distinction: work that should continue after the HTTP request ends should not depend on the request’s cancellation token.
For example, if an API accepts a request to generate a large report overnight, the application should generally enqueue a background job and return a job identifier. The background worker should manage its own cancellation and lifetime rather than relying on the original request token.
Common Mistakes to Avoid
| Mistake | Why it causes problems | Better approach |
|---|---|---|
| Receiving a token but not passing it onward | Lower layers cannot respond to cancellation | Propagate the token through service and repository methods |
| Assuming cancellation forcibly kills an operation | .NET cancellation is cooperative | Check the token and use APIs that support cancellation |
| Ignoring HTTP client cancellation | Outgoing calls may continue unnecessarily | Pass the token to HttpClient operations |
| Treating cancellation as a server error | Logs become noisy and misleading | Handle expected cancellation separately |
| Assuming a cancelled write was rolled back | The database operation may already have completed | Use transaction and idempotency strategies where needed |
| Using the request token for background jobs | Work may stop as soon as the request ends | Give background work an independent lifetime |
| Swallowing cancellation exceptions | Processing may continue unexpectedly | Rethrow cancellation after required cleanup |
Troubleshooting CancellationToken Problems
The database query continues after the client disconnects
Check whether the cancellation token reaches the EF Core query and whether the database provider supports cancellation. Inspect database activity to determine whether the command actually stops.
An external API call keeps running
Verify that the token is passed to the outgoing HTTP request and any subsequent response-content reads. Check the downstream service separately because cancelling the client request does not guarantee that remote processing stops.
Cancellation appears as an error in application logs
Review exception handling and logging filters. Expected request cancellations can often be logged at a lower level or omitted from error reporting, while unexpected failures should still be recorded.
Background work stops unexpectedly
Check whether the background operation uses HttpContext.RequestAborted. If the work must survive the request, enqueue it in a background processing system and give it a separate cancellation policy.
Best Practices for Production Applications
- Accept a
CancellationTokenin asynchronous request-handling methods. - Pass the token through service, repository, database, and HTTP client calls.
- Check cancellation during long-running CPU-bound work.
- Use linked tokens when request cancellation and operation deadlines must both apply.
- Distinguish timeouts from client-request cancellation when the difference matters.
- Treat cancellation as cooperative, not as a guarantee that all work stops immediately.
- Use transactions and idempotency where important writes may have uncertain outcomes.
- Keep background jobs independent of the HTTP request lifetime when they must continue after the response.
- Avoid logging expected cancellation as an application failure unless the context warrants it.
- Test cancellation during database queries, external HTTP calls, streaming responses, and long-running calculations.
Advantages and Disadvantages of CancellationToken
Advantages
- Reduces unnecessary work after a request is aborted.
- Helps release resources sooner when operations support cancellation.
- Supports coordinated cancellation across multiple asynchronous operations.
- Allows applications to enforce operation deadlines.
- Makes long-running operations more responsive to shutdown and cancellation requests.
Disadvantages
- Requires cooperation from the code and libraries involved.
- Does not guarantee that a remote operation or database write is rolled back.
- Needs to be passed through the application call chain.
- Can complicate error handling when cancellation and timeouts need different responses.
Summary
CancellationToken helps ASP.NET Core applications stop unnecessary work when a request is aborted or an operation reaches its deadline. However, cancellation works only when the relevant code and underlying APIs observe the token.
Pass the request cancellation token through database queries, external HTTP calls, and long-running operations. Use linked tokens for timeouts, handle cancellation without hiding it, and keep background jobs independent of the request lifetime when necessary.
The key is to design cancellation into the complete operation rather than adding a token only to the controller method. This helps reduce wasted resources and makes asynchronous applications more predictable under load.
ASP.NET Core 11.0 Hosting Recommendation
HostForLIFE.eu
HostForLIFE.eu is a popular recommendation that offers various hosting choices. Starting from shared hosting to dedicated servers, you will find options fit for beginners and popular websites. It offers various hosting choices if you want to scale up. Also, you get flexible billing plans where you can choose to purchase a subscription even for one or six months.
