Pass the CancellationToken or the Work Continues After the Client Left
The browser closed the tab. The load balancer timed out. The caller retried. Your action is still in ToListAsync, and the connection it borrowed is still checked out. ASP.NET Core already knows the request is over. It exposes that as HttpContext.RequestAborted. A CancellationToken parameter on the action is bound to that token. If you do not pass it down, nothing downstream is listening.
Forward the token you were given
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id, CancellationToken cancellationToken)
{
var order = await db.Orders
.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
if (order is null) return NotFound();
return Ok(order);
}
The same argument goes to SaveChangesAsync, to HttpClient.SendAsync, and to any channel read you own. A helper that accepts CancellationToken cancellationToken = default and is called without one will run to its own timeout. default is CancellationToken.None. It never fires because the client left.
A deadline and the request token compose with a linked source. Cancel when either fires.
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linked.CancelAfter(TimeSpan.FromSeconds(3));
await client.SendAsync(request, linked.Token);
The three-second cap still applies when the client is waiting. The request token still applies when the client is not. One token has to represent both, because SendAsync takes one.
A cancel is not a server failure
RequestAborted throws OperationCanceledException (often as TaskCanceledException) out of the call you passed it to. If a broad catch (Exception) turns that into HTTP 500, the logs fill with errors for people who navigated away, and the retry on the client treats a disconnect as a failed write. Let that exception propagate when the request is aborted. Do not log it as an application fault.
The awkward case is SaveChangesAsync. If the token fires after the command reached the database, the client sees a cancel and the row may have committed. You cannot tell from the exception alone. A retry of that POST needs an idempotency key, or the retry inserts a second row. The key design is the same one described for write APIs.
What you are actually saving
Each abandoned request holds a pooled connection until the query finishes. Under a disconnect storm that is how the pool saturates while the endpoint looks healthy. HttpClient has the same shape: a call that ignores cancellation sits until the handler times out, which is the pressure case next to creating a client per request. Passing the token does not make the database faster. It lets the work stop when nobody is waiting for it.
Keep reading
IHttpClientFactory: Stop Creating HttpClient Per Request
Why new HttpClient() in a .NET service exhausts sockets under load, and how IHttpClientFactory plus a named client fixes DNS without freezing the handler forever.
CQRS in Practice: When It's Worth the Complexity and When It Isn't
A clear-eyed look at Command Query Responsibility Segregation — what it actually is, how to implement it in .NET, when it pays off, and when it's unnecessary complexity.
Fixing NuGet Error: Unable to Load the Service Index for Source
A step-by-step guide to diagnosing and resolving NuGet package source errors in Visual Studio and dotnet CLI.
Dataverse Plug-in Performance: Avoiding the Slow Plug-in That Times Out
A synchronous plug-in runs inside the user's transaction with a hard time limit. One slow query or external call can break saves for everyone. Here is how to keep plug-ins fast and safe.
Next.js 16 Cache Components: Replacing revalidate and force-dynamic With 'use cache'
How Cache Components in Next.js 16 replace route segment configs with the use cache directive and cacheLife, what becomes a build error, and the cookies-outside-the-cache rule that trips every migration.
TypeScript 7 Upgrade: 10x Faster tsc, and the API Gap You Have to Plan Around
TypeScript 7.0 is the native Go compiler, stable since July 2026, with 8 to 12x faster full builds. Deprecations are hard errors, strict and esnext are defaults, and the programmatic API waits for 7.1. A migration order that keeps lint and frameworks working.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
- No comments yet. Be the first.