C# CancellationToken Best Practices for Web API

2026/05/154 min read
bookmark this

Table of Contents

  1. Introduction
  2. Why Cancellation Matters in Web APIs
  3. How CancellationToken Works in ASP.NET Core
  4. Best Practices by Layer
  5. Demo: Detecting Cancellation in Practice
  6. Testing Cancellation: Client Scenarios
  7. Summary Checklist
  8. 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 CancellationToken into 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 OperationCanceledException at 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.

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 IsCancellationRequested for 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 CancellationToken through 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 OperationCanceledException for observability

References


For more C# and ASP.NET Core tips, check out other posts in this category!