Cancellation is an important part of building reliable ASP.NET Core applications. An HTTP request may be canceled because a user closes a browser tab, a client times out, or a downstream service stops waiting for a response. Background operations may also need to stop when an application shuts down.
If your application ignores cancellation, it may continue running database queries, waiting for HTTP responses, or processing data that nobody needs anymore. Under heavy traffic, this wasted work can consume database connections, memory, threads, and other resources.
.NET provides CancellationToken to let one part of an application signal that an operation should stop. However, passing a token to a method does not automatically guarantee cancellation. The operation must observe the token, and the underlying API must support cancellation.
This article explains how to use CancellationToken correctly in ASP.NET Core, how to pass it through asynchronous operations, and how to avoid common mistakes.
What Is CancellationToken?
A CancellationToken communicates a cancellation request. It does not forcibly terminate a running operation. Instead, the operation checks the token and stops when it can do so safely.
For example, an asynchronous method can accept a cancellation token:
public async Task<string> GetDataAsync(
CancellationToken cancellationToken)
{
await Task.Delay(
TimeSpan.FromSeconds(5),
cancellationToken);
return "Data loaded";
}
The Task.Delay method observes the token. If cancellation is requested before the delay finishes, the operation throws an OperationCanceledException rather than returning the result.
The token itself does not perform the cancellation. A CancellationTokenSource controls when cancellation is requested.
using var cancellationSource =
new CancellationTokenSource();
cancellationSource.Cancel();
CancellationToken token =
cancellationSource.Token;
In an ASP.NET Core application, you usually do not need to create a token source for every incoming request. The framework provides a request cancellation token through HttpContext.RequestAborted.
Use RequestAborted in ASP.NET Core
ASP.NET Core exposes HttpContext.RequestAborted, which is canceled when the server detects that the request has been aborted.
Minimal API endpoints can receive a CancellationToken parameter directly. ASP.NET Core binds it to the request's cancellation token.
app.MapGet("/api/reports", async (
ReportsDbContext db,
CancellationToken cancellationToken) =>
{
var reports = await db.Reports
.AsNoTracking()
.ToListAsync(cancellationToken);
return Results.Ok(reports);
});
The token is passed to ToListAsync(), allowing Entity Framework Core to request cancellation of the database operation when the request is aborted.
Without the token, the database query may continue even after the client no longer needs the result.
This is especially important for endpoints that perform expensive queries or call external services. Propagating cancellation helps the application stop unnecessary work as early as its dependencies allow.
For controller-based applications, you can use the same approach:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
[ApiController]
[Route("api/reports")]
public class ReportsController : ControllerBase
{
private readonly ReportsDbContext _db;
public ReportsController(ReportsDbContext db)
{
_db = db;
}
[HttpGet]
public async Task<IActionResult> GetReports(
CancellationToken cancellationToken)
{
var reports = await _db.Reports
.AsNoTracking()
.ToListAsync(cancellationToken);
return Ok(reports);
}
}
ASP.NET Core supplies the request cancellation token to the action parameter. The controller does not need to retrieve it manually from HttpContext.RequestAborted.
Pass Cancellation Tokens Through Every Layer
A common mistake is accepting a cancellation token in the endpoint but not passing it to the service or repository that performs the actual work.
Consider this service method:
public async Task<Order?> GetOrderAsync(int orderId)
{
return await _db.Orders
.FirstOrDefaultAsync(order => order.Id == orderId);
}
The endpoint might receive a cancellation token, but this method cannot use it. The token is lost before the database operation begins.
Update the method to accept and forward the token:
public async Task<Order?> GetOrderAsync(
int orderId,
CancellationToken cancellationToken)
{
return await _db.Orders
.FirstOrDefaultAsync(
order => order.Id == orderId,
cancellationToken);
}
The endpoint can then pass its token through the service:
app.MapGet("/api/orders/{id:int}", async (
int id,
OrderService orderService,
CancellationToken cancellationToken) =>
{
var order = await orderService.GetOrderAsync(
id,
cancellationToken);
return order is null
? Results.NotFound()
: Results.Ok(order);
});
This creates a cancellation path from the incoming HTTP request to the database operation.
The same principle applies to application services, repositories, HTTP clients, file operations, and other asynchronous work. Every layer that can cooperate with cancellation should receive the token and pass it to the relevant API.
Cancel Outgoing HTTP Requests
ASP.NET Core applications often call other APIs. If a client disconnects while your application is waiting for an external service, continuing the outgoing request may waste resources.
Use HttpClient methods that accept a cancellation token:
public sealed class InventoryService
{
private readonly HttpClient _httpClient;
public InventoryService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetInventoryAsync(
CancellationToken cancellationToken)
{
using var response = await _httpClient.GetAsync(
"/api/inventory",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(
cancellationToken);
}
}
The token is passed to both the HTTP request and the content-reading operation.
If cancellation is requested, the HTTP operation can stop according to the behavior of the HTTP stack and the remote connection. It does not guarantee that the remote server will stop processing a request it has already received.
Register the typed client in Program.cs:
builder.Services.AddHttpClient<InventoryService>(client =>
{
client.BaseAddress =
new Uri("https://inventory.example.internal");
});
The base address is an illustrative value. Replace it with the address of your actual service.
Do not create a new HttpClient for every request. Use IHttpClientFactory or another appropriate client lifetime strategy so connections can be managed efficiently.
Distinguish Cancellation from Errors
Cancellation is not always the same as an application failure.
For example, an HTTP request may be canceled because the client disconnected. A database query may be canceled because the request is no longer needed. These events do not necessarily indicate that your application is malfunctioning.
At the same time, not every OperationCanceledException means the client disconnected. An operation may use its own cancellation token or have a timeout that triggers cancellation.
Avoid treating every cancellation as an unexpected server error.
app.MapGet("/api/data", async (
DataService dataService,
CancellationToken cancellationToken) =>
{
try
{
var result = await dataService.GetDataAsync(
cancellationToken);
return Results.Ok(result);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
throw;
}
});
In this example, the exception is rethrown when the request token has been canceled. In many ASP.NET Core endpoints, the explicit try and catch are unnecessary because the framework already handles an aborted request. The example demonstrates that cancellation should not automatically be converted into a normal success response.
Do not catch OperationCanceledException and return an empty list or a successful response just to hide the exception. Doing so can make monitoring misleading and may cause clients to treat incomplete work as successful.
Also avoid logging every expected client disconnect as an application error. Record cancellation events when they help diagnose behavior, but keep them separate from genuine application failures.
Use Linked Tokens for Request Limits
Sometimes an operation should stop either when the HTTP request is canceled or when an application-defined time limit is reached.
A linked cancellation token source combines multiple cancellation signals.
app.MapGet("/api/slow-report", async (
ReportService reportService,
CancellationToken requestToken) =>
{
using var timeoutSource =
new CancellationTokenSource(
TimeSpan.FromSeconds(10));
using var linkedSource =
CancellationTokenSource.CreateLinkedTokenSource(
requestToken,
timeoutSource.Token);
var report = await reportService.GenerateAsync(
linkedSource.Token);
return Results.Ok(report);
});
The operation receives a token that is canceled when either the request token or the ten-second timeout token is canceled.
This pattern is useful when a particular operation needs a stricter time limit than the overall HTTP request.
However, the code above does not distinguish which token triggered cancellation. If that distinction matters for logging or response handling, check the original tokens or use a timeout strategy that reports the cause explicitly.
A timeout also does not guarantee that an operation stops immediately. The underlying API must support cancellation, and some operations may take time to respond.
Handle Cancellation in Loops and Streaming Operations
Long-running loops should periodically check for cancellation, especially when they perform synchronous work between asynchronous calls.
public async Task ProcessOrdersAsync(
IEnumerable<Order> orders,
CancellationToken cancellationToken)
{
foreach (var order in orders)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessOrderAsync(
order,
cancellationToken);
}
}
The token is checked before processing each order and passed into the processing method. This lets the application stop between items and allows the individual operation to stop when supported.
For asynchronous streams, use cancellation-aware enumeration:
public async Task ExportOrdersAsync(
IAsyncEnumerable<Order> orders,
CancellationToken cancellationToken)
{
await foreach (var order in orders
.WithCancellation(cancellationToken))
{
await WriteOrderAsync(
order,
cancellationToken);
}
}
The stream's producer must also support cancellation for it to stop retrieving data promptly. Passing a token to the consumer cannot guarantee that an arbitrary producer will stop its internal work.
Avoid checking cancellation only after a large batch has completed. For expensive operations, check it at appropriate boundaries so the application does not continue unnecessary work for too long.
Common CancellationToken Mistakes
Accepting a token but ignoring it
A method that accepts a token but never checks or forwards it provides little benefit. Pass the token to asynchronous APIs that support cancellation.
Using Task.Run for ordinary asynchronous I/O
Wrapping database or HTTP calls in Task.Run() does not make them more cancellable. Use the asynchronous APIs directly and pass the token through them.
Swallowing cancellation exceptions
Returning a success response after an operation is canceled can hide incomplete work. Handle cancellation according to the operation's purpose and the framework's request lifecycle.
Assuming cancellation rolls back database changes
Cancellation does not automatically reverse changes that have already been committed. If an operation modifies data, use appropriate database transactions and application-level consistency rules.
Reusing a canceled token
Once a CancellationTokenSource has been canceled, its token remains canceled. Create a new source for a new independent operation rather than trying to reuse a canceled source.
Forgetting background work has a different lifetime
An HTTP request token should not be used to control work that must continue after the request finishes. Background jobs should have their own lifecycle and cancellation strategy, usually connected to application shutdown and job-specific cancellation.
Request Cancellation vs. Application Shutdown
Request cancellation and application shutdown represent different events.
HttpContext.RequestAborted is associated with an individual HTTP request. IHostApplicationLifetime.ApplicationStopping signals that the host is shutting down.
For work that belongs to a hosted service, use the cancellation token supplied to BackgroundService.ExecuteAsync or the appropriate hosted-service lifecycle method.
Do not tie a durable background job to an HTTP request token if the job is expected to continue after the client disconnects. Instead, enqueue the work and let a background worker manage its own lifetime.
Conclusion
CancellationToken helps ASP.NET Core applications stop unnecessary work when requests are aborted, operations time out, or the application shuts down. Its effectiveness depends on passing the token through the application layers and using APIs that support cancellation.
Start with the request token, forward it to database and HTTP operations, check it during long-running loops, and distinguish cancellation from genuine application errors. For work that must outlive an HTTP request, use a separate background-processing lifecycle.
These practices improve resource usage and make asynchronous operations more predictable under load.
Jasen FiciPosted Oct 21, 2026, 7:21 AM
Appreciate you writing this. It's featured in DotNetNews: https://dotnetnews.beehiiv.com/p/new-post-1407