Pass your CancellationToken all the way down
When a caller gives up on a request, your code should stop working on it too. It only takes a parameter, but you have to pass it through every layer.
A client calls your API, waits a few seconds, and gives up. The browser tab closes, or the upstream service hits its own timeout. On the server, your code carries on: it finishes the database query, calls two downstream APIs, maps the results and writes a response nobody will read.
That wasted work adds up under load, and it's easy to avoid. .NET already tells you when the caller has gone. You just have to listen.
Where the token comes from
In ASP.NET Core, add a CancellationToken parameter to an action or a minimal API handler and the framework binds it to HttpContext.RequestAborted. It is cancelled when the client disconnects.
app.MapGet("/orders/{id:int}", async (int id, OrdersService orders, CancellationToken ct) =>
{
var order = await orders.GetAsync(id, ct);
return order is null ? Results.NotFound() : Results.Ok(order);
});
Background services get one too: the stoppingToken passed to ExecuteAsync is cancelled when the host shuts down.
Pass it through every layer
A token only helps if it reaches the code that does the slow work. Every async method in between should accept one and hand it on:
public async Task<Order?> GetAsync(int id, CancellationToken ct)
{
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id, ct);
if (order is null) return null;
order.Shipping = await shippingClient.GetStatusAsync(order.TrackingNumber, ct);
return order;
}
EF Core, HttpClient, Stream and most Azure SDK clients take a token on their async methods. If one method in the chain doesn't pass it on, everything below that point keeps running after the caller has gone.
Add your own timeout
Sometimes you want to give up before the caller does, for example when a downstream API is slow. Link a timeout to the incoming token so that either one cancels the work:
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5));
var status = await shippingClient.GetStatusAsync(trackingNumber, cts.Token);
Don't treat cancellation as an error
When a token is cancelled, the awaited call throws OperationCanceledException (or TaskCanceledException, which derives from it). That is expected, so don't log it as a failure or retry it:
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
// The caller went away. Nothing to report.
}
The when filter matters. If your own timeout fired instead, ct isn't cancelled, and you probably do want to log that.
Tip: turn on the CA2016 analyzer rule. It flags async calls that could take the token you already have but don't.
Takeaway
Accept a CancellationToken in every async method and pass it to every call that takes one. It costs one parameter, and your service stops doing work nobody is waiting for.