C# CancellationToken Best Practices for Web API
Table of Contents
- Introduction
- Why Cancellation Matters in Web APIs
- How CancellationToken Works in ASP.NET Core
- Best Practices by Layer
- Demo: Detecting Cancellation in Practice
- Testing Cancellation: Client Scenarios
- Summary Checklist
- References
Introduction
Handling request cancellation is essential for building robust, scalable ASP.NET Core Web APIs. This post covers the best practices for using CancellationToken—from API to service and data layers—so your app can respond quickly to client disconnects and avoid wasted work.
Why Cancellation Matters in Web APIs
When a client disconnects (closes a browser tab, navigates away, or cancels a request), the server should stop any unnecessary work. ASP.NET Core provides a CancellationToken for every request, allowing you to:
- Free up resources immediately
- Avoid running expensive or irreversible operations
- Improve scalability and responsiveness
How CancellationToken Works in ASP.NET Core
- ASP.NET Core injects a
CancellationTokeninto every controller action. - The token is triggered if the client disconnects or cancels the request.
- Many async APIs (like
Task.Delay,HttpClient, EF Core) already respect the token.
Best Practices by Layer
Based on expert guidance:
- API Layer:
- Never check the token manually. ASP.NET injects and manages it for you.
- Only catch
OperationCanceledExceptionat the top level if you want to log or return a custom status code.
- Service Layer:
- Add manual checks only:
- Before starting slow or critical work (in case the client already disconnected)
- Between long-running steps or before irreversible actions (e.g., sending an email)
- Use
cancellationToken.ThrowIfCancellationRequested();where appropriate. - Never check manually. Most async APIs (EF Core,
Task.Delay,HttpClient) already handle the token.
- Add manual checks only:
Mental test:
"Am I doing slow work that the framework can't see?" If yes, add a check. If not, just pass the token.
Demo: Detecting Cancellation in Practice
IsCancellationRequested vs ThrowIfCancellationRequested
When should you use cancellationToken.IsCancellationRequested instead of ThrowIfCancellationRequested()?
- Use IsCancellationRequested when you want to check for cancellation and exit gracefully—without throwing an exception. This is ideal for loops, background services, or cleanup code where you can simply return or break.
- Use ThrowIfCancellationRequested() when you want to immediately abort execution and bubble up an
OperationCanceledException. This is best for service/business logic where you want to stop work and let higher layers handle the cancellation (logging, status code, etc.).
Summary:
- Use
IsCancellationRequestedfor silent, graceful exits. - Use
ThrowIfCancellationRequested()to enforce immediate cancellation and propagate the exception.
Example:
// Graceful exit in a loop
while (!cancellationToken.IsCancellationRequested)
{
await DoWorkAsync();
}
// Immediate abort in business logic
cancellationToken.ThrowIfCancellationRequested();
DoCriticalWork();
Backend API Example
[HttpGet("slow-report")]
public async Task<IActionResult> GetSlowReport(CancellationToken cancellationToken)
{
_logger.LogInformation(">>> Request started");
cancellationToken.ThrowIfCancellationRequested();
try
{
// Step 1 — simulate slow DB query
_logger.LogInformation(">>> Starting DB query...");
await Task.Delay(5_000, cancellationToken);
_logger.LogInformation(">>> DB query done");
// Step 2 — simulate heavy CPU work
_logger.LogInformation(">>> Building report...");
await Task.Delay(5_000, cancellationToken);
_logger.LogInformation(">>> Report built");
return Ok(new { message = "Done" });
}
catch (OperationCanceledException)
{
_logger.LogWarning(">>> REQUEST CANCELLED — client disconnected");
return StatusCode(499);
}
}
- If the client cancels after 2 seconds, logs show:
>>> Request started>>> Starting DB query...>>> REQUEST CANCELLED — client disconnected(never reaches "DB query done")
Testing Cancellation: Client Scenarios
1. Manual Cancellation with HttpClient
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(2));
using var http = new HttpClient();
try
{
var response = await http.GetAsync("https://localhost:7001/api/slow-report", cts.Token);
Console.WriteLine($"Completed: {response.StatusCode}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Cancelled — simulates client disconnect");
}
- This approach is repeatable and precise for testing.
2. Browser Tab Close / Navigation
fetch('https://localhost:7001/api/slow-report')
.then(r => r.json())
.then(console.log);
// Now close the tab — ASP.NET Core detects the TCP connection drop and fires the CancellationToken
3. UI Demo (Sample Client)
The included demo client lets you:
- Start a long-running request
- Cancel manually or simulate a tab close
- View real-time logs of cancellation events
Summary Checklist
- Pass the
CancellationTokenthrough all async APIs - Only check/cancel in the service layer when needed
- Never check in API or data layers, because framework code already handler
- Log and handle
OperationCanceledExceptionfor observability
References
For more C# and ASP.NET Core tips, check out other posts in this category!