An API request does not always complete successfully. A user may close a browser tab, a mobile application may lose its network connection, or an HTTP client may cancel a request while the server is still processing it.
Consider an API endpoint that generates a report by querying thousands of database records. The client disconnects after a few seconds, but the database query continues running because the application never passes the request cancellation token to the database operation.
The server is now doing work that nobody is waiting for. Under heavy traffic, these unnecessary queries can consume database connections, CPU, memory, and connection-pool capacity. They can also delay requests from users who are still connected.
ASP.NET Core provides request cancellation through HttpContext.RequestAborted. When this cancellation token is propagated through the application layers and into supported database operations, work can stop when the request is cancelled.
However, cancellation is cooperative: it requests that an operation stop; it does not guarantee that every database provider or command will terminate immediately. Understanding where to pass the token, how EF Core handles cancellation, and how to distinguish client disconnects from application failures is essential for building responsive APIs.
Why Database Queries Continue After a Client Disconnects
Consider an API that retrieves customer orders and returns them to a mobile application.
[HttpGet("customers/{customerId}/orders")]
public async Task<IActionResult> GetOrders(int customerId)
{
var orders = await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
return Ok(orders);
}The endpoint works correctly when the client stays connected.
However, imagine that the database takes 15 seconds to execute the query and the user closes the application after 3 seconds.
What happens next?
The HTTP connection may be closed, but the database operation has no cancellation token. The application has not explicitly requested that the query stop.
Depending on the database provider and execution state, the query can continue consuming resources even though its result is no longer needed by the client.
This becomes a serious concern when the application handles many concurrent requests.
For example:
Users repeatedly navigate away from pages that trigger expensive queries.
Mobile applications cancel requests when network connectivity changes.
Frontend applications cancel previous searches when users enter new search terms.
API gateways terminate requests after their configured timeout.
Clients cancel requests when users switch between screens.
Without cancellation propagation, the application may continue executing unnecessary database operations.
The solution is to connect the lifetime of the HTTP request to the lifetime of the database operation.
Understand HttpContext.RequestAborted
ASP.NET Core provides the RequestAborted property through HttpContext.
It exposes a CancellationToken that is triggered when the underlying HTTP request is aborted by the client or server.
For example:
CancellationToken cancellationToken =
HttpContext.RequestAborted;You can use this token to notify long-running operations that the request is no longer active.
A simplified execution flow looks like this:
Client sends HTTP request
|
v
ASP.NET Core receives request
|
v
Controller starts database query
|
v
Client disconnects
|
v
RequestAborted is triggered
|
v
CancellationToken is propagated
|
v
EF Core / Database Provider
|
v
Database operation receives cancellationThe important point is that ASP.NET Core does not automatically cancel every database operation just because a client disconnects.
The application must pass the cancellation token to the operations that support it.
RequestAborted is also not limited to browser disconnections. It can be triggered when the server itself aborts the request.
Pass CancellationToken Directly to Controller Actions
One of the simplest ways to implement request cancellation is to accept CancellationToken as an action parameter.
ASP.NET Core binds this parameter to the request cancellation token.
You do not need to access HttpContext manually.
For example:
[HttpGet("customers/{customerId}/orders")]
public async Task<IActionResult> GetOrders(
int customerId,
CancellationToken cancellationToken)
{
var orders = await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync(cancellationToken);
return Ok(orders);
}The cancellation token is now passed directly to ToListAsync.
When the request is cancelled, EF Core receives the cancellation request and passes it to the underlying database provider.
This is a small code change, but it can make a significant difference in applications that execute expensive queries.
Why use a CancellationToken parameter instead of HttpContext?
There are two ways to obtain the request cancellation token:
var token = HttpContext.RequestAborted;Or:
public async Task<IActionResult> GetOrders(
CancellationToken cancellationToken)The second approach is generally cleaner for controller actions because it makes cancellation an explicit dependency of the operation.
It also avoids passing HttpContext into application services, which should generally remain independent of ASP.NET Core request infrastructure.
For Minimal APIs, a CancellationToken parameter is also bound to the request cancellation token.
app.MapGet(
"/orders/{customerId:int}",
async (
int customerId,
AppDbContext context,
CancellationToken cancellationToken) =>
{
var orders = await context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync(cancellationToken);
return Results.Ok(orders);
});This makes request cancellation straightforward in both controller-based APIs and Minimal APIs.
Propagate Cancellation Through Every Application Layer
Passing a cancellation token to the controller is not enough if the application service ignores it.
Consider a typical application architecture:
Controller
|
v
Application Service
|
v
Repository
|
v
Entity Framework Core
|
v
DatabaseIf the controller receives a cancellation token but the repository does not accept it, the cancellation request cannot reach the database operation.
A common mistake is to implement a service like this:
public async Task<List<Order>> GetOrdersAsync(int customerId)
{
return await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
}The method does not accept a cancellation token.
A better implementation is:
public async Task<List<Order>> GetOrdersAsync(
int customerId,
CancellationToken cancellationToken)
{
return await _context.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync(cancellationToken);
}The controller can then call the service:
[HttpGet("customers/{customerId}/orders")]
public async Task<IActionResult> GetOrders(
int customerId,
CancellationToken cancellationToken)
{
var orders = await _orderService.GetOrdersAsync(
customerId,
cancellationToken);
return Ok(orders);
}The service and repository should continue passing the same token until it reaches the database operation.
A consistent method signature makes cancellation easier to maintain throughout the application.
For example:
public interface IOrderRepository
{
Task<List<Order>> GetOrdersAsync(
int customerId,
CancellationToken cancellationToken);
}Avoid creating a new CancellationTokenSource inside every repository method unless there is a specific reason to introduce a separate cancellation boundary.
The original request token should normally be propagated rather than replaced.
Understand How EF Core Handles Cancellation
EF Core provides asynchronous versions of database operations, including:
ToListAsync
FirstOrDefaultAsync
SingleAsync
AnyAsync
CountAsync
SaveChangesAsync
ExecuteUpdateAsync
ExecuteDeleteAsync
These methods accept CancellationToken parameters.
For example:
var order = await _context.Orders
.FirstOrDefaultAsync(
o => o.Id == orderId,
cancellationToken);For a database update:
var affectedRows = await _context.Orders
.Where(o => o.Status == "Pending")
.ExecuteUpdateAsync(
setters => setters.SetProperty(
o => o.Status,
"Expired"),
cancellationToken);For saving tracked changes:
await _context.SaveChangesAsync(cancellationToken);In each case, the cancellation token is passed through EF Core to the database provider.
However, there is an important limitation: database providers may or may not honor cancellation in every situation.
Even when a provider supports cancellation, the database may need some time to receive and process the cancellation request.
Therefore, cancellation should be treated as a request to stop work, not an absolute guarantee that the database immediately terminates the operation.
You should also keep command timeouts configured appropriately. Cancellation and command timeout solve related but different problems.
Cancellation allows the caller to request that an operation stop.
Command timeout limits how long a database command can execute before the provider reports a timeout.
Using both provides better control over database operations.
Handle Cancellation Exceptions Without Logging Them as Application Failures
When a database operation is cancelled, the application may receive an OperationCanceledException or a provider-specific cancellation exception.
This is expected behavior.
Consider this implementation:
try
{
var orders = await _context.Orders
.ToListAsync(cancellationToken);
return Ok(orders);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve orders");
return StatusCode(500);
}This catches every exception, including expected request cancellation.
As a result, normal client disconnections may appear as application errors in production logs.
A better approach is to handle cancellation separately:
try
{
var orders = await _context.Orders
.ToListAsync(cancellationToken);
return Ok(orders);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation(
"Order retrieval was cancelled because the request ended.");
throw;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"An unexpected error occurred while retrieving orders.");
return StatusCode(500);
}This implementation distinguishes request cancellation from unexpected application errors.
You generally should not attempt to return a normal HTTP response after the client has disconnected. Rethrowing allows the request pipeline to finish handling the cancelled operation.
Also, do not assume every OperationCanceledException means the client disconnected. An operation may have been cancelled by an application-level timeout or another cancellation source.
Check the relevant token to understand why cancellation occurred.
Use Cancellation with Long-Running Queries and Large Result Sets
Cancellation is particularly useful when an API retrieves large datasets.
Consider an endpoint that returns customer transaction history:
[HttpGet("transactions")]
public async Task<IActionResult> GetTransactions(
int customerId,
CancellationToken cancellationToken)
{
var transactions = await _context.Transactions
.Where(t => t.CustomerId == customerId)
.OrderByDescending(t => t.CreatedAt)
.ToListAsync(cancellationToken);
return Ok(transactions);
}The application waits for the complete result set before returning the response.
If the query returns hundreds of thousands of records, the operation may consume significant database and application resources.
Passing a cancellation token allows EF Core to request cancellation while executing the query.
However, cancellation is only one part of the solution.
You should also consider:
Adding pagination.
Selecting only the required columns.
Applying appropriate indexes.
Avoiding unnecessary entity tracking.
Limiting the maximum number of returned records.
For example:
Join the conversation! Your thoughts help the community grow.