Skip to main content

Command Palette

Search for a command to run...

CancellationToken in ASP.NET Core: Enterprise Decision Guide for High-Throughput APIs

Published
β€’7 min read
CancellationToken in ASP.NET Core: Enterprise Decision Guide for High-Throughput APIs

Enterprise teams that build high-throughput ASP.NET Core APIs often treat CancellationToken as a checkbox β€” something you add to satisfy the compiler or satisfy a code review comment. That approach misses the real governance question: when does propagating a CancellationToken actually protect your system, and when does ignoring one silently cause worse problems than the cancelled request would have?

Want implementation-ready .NET source code you can adapt fast? Join Coding Droplets on Patreon. πŸ‘‰ https://www.patreon.com/CodingDroplets

What CancellationToken Actually Represents in Enterprise APIs

A CancellationToken in ASP.NET Core carries a signal, not a guarantee. When a client disconnects, the browser tab closes, or a load balancer times out the upstream request, HttpContext.RequestAborted is cancelled. That token propagates to your controller action, which propagates it β€” if you let it β€” to your database calls, outbound HTTP calls, and queue publish operations.

The system does not automatically stop work in progress. Your code decides whether to check the token and stop, or ignore it and finish anyway. That decision has real infrastructure cost consequences at scale.

The Three Propagation Patterns Enterprise Teams Actually Use

Full propagation means threading CancellationToken from the controller action through every awaitable call in the request path β€” EF Core queries, HttpClient calls, message broker publishes. This is the correct default for read operations and non-critical writes where a half-completed operation causes no data integrity risk.

Selective propagation means using HttpContext.RequestAborted for the read path but using CancellationToken.None or a separate application-level token for write operations. This is the right pattern when you need the database write to complete even if the client disconnected β€” for example, a payment initiation or order creation where a half-committed write is worse than a redundant one.

Linked tokens combine HttpContext.RequestAborted with an application-level timeout token using CancellationTokenSource.CreateLinkedTokenSource. This enforces both a per-request deadline and a client-disconnect signal, giving you bounded execution without relying on the client to stay connected.

When to Propagate vs When to Ignore

The decision framework is straightforward when framed around outcome risk:

Propagate when:

  • The operation is a read β€” a cancelled read means no data was mutated and no cleanup is needed
  • The downstream operation supports cooperative cancellation cleanly (EF Core, HttpClient, and most modern .NET libraries do)
  • The cost of completing unnecessary work exceeds the cost of the occasional OperationCanceledException in your logs
  • You are calling external APIs where request cancellation prevents you from being charged for a completed call

Ignore (use CancellationToken.None) when:

  • The operation writes state that must be durable regardless of client presence β€” financial transactions, audit records, outbox event inserts
  • The operation has compensating logic that is more expensive than just completing β€” rolling back a Saga step that already triggered downstream effects
  • You are inside a background service or hosted worker where the token comes from application shutdown, not request cancellation

EF Core: The Most Common Propagation Mistake

The most frequent pattern mistake in enterprise codebases is passing HttpContext.RequestAborted to both the read query and the subsequent write command in the same request handler. The read cancellation is fine. The write cancellation is not.

If the client disconnects after the read completes but before the write commits, and you've passed the request token to SaveChangesAsync, EF Core throws an OperationCanceledException after the database transaction may have partially committed. This is particularly dangerous with explicit transactions β€” the rollback may not fire cleanly, or the connection may return to the pool in an ambiguous state.

The governance rule: pass cancellationToken to read operations and CancellationToken.None to SaveChangesAsync and any CommitAsync calls when those writes represent durable business state changes.

HttpClient and Outbound API Calls

HttpClient respects CancellationToken fully β€” a cancelled token will abort the outbound HTTP request at the socket level if the response hasn't been received yet. This is the correct behavior for read-oriented API calls.

For write-oriented outbound calls (creating a resource in a third-party system, initiating a payment, sending a notification), the cancellation semantics depend on the external API. Some external APIs are idempotent and safe to retry; others are not. If you cancel an outbound write and the request already reached the external system, you may have a dangling operation with no local record. Propagating the token to idempotent writes is safe. Propagating it to non-idempotent writes requires explicit handling of the cancellation path.

Message Queue Publish Operations

Publishing to a message broker (Azure Service Bus, RabbitMQ, Kafka) during a request should be evaluated the same way as a database write. If the message represents a business event that must be delivered regardless of client presence, use CancellationToken.None or an application-level token tied to application shutdown rather than HttpContext.RequestAborted.

If you're using the Outbox pattern correctly β€” writing to the outbox table inside the same EF Core transaction as your domain state change β€” the publish to the broker happens out-of-band in a relay process, and the request-scoped token never reaches the broker call. This is the cleanest approach for enterprise systems with reliability requirements.

Background Services and Hosted Workers

IHostedService and BackgroundService receive a CancellationToken that fires on application shutdown, not on request cancellation. This is a different lifecycle entirely. Confusing the two is a common mistake when refactoring request-scoped code into background services.

In hosted workers, the token should propagate to all awaitable operations to ensure clean shutdown when the application is stopping. Setting CancellationToken.None in a background service means the worker will not stop cleanly during rolling deploys or Kubernetes pod termination.

Enterprise Governance Checklist

Before standardizing CancellationToken usage across your team, lock in these decisions:

  • Read path: Always propagate HttpContext.RequestAborted to all read queries and outbound read calls
  • Write path: Evaluate each write operation individually β€” use CancellationToken.None for durable writes, request token for idempotent or reversible writes
  • Timeout policy: Use linked tokens to enforce per-request timeouts independently of client connectivity
  • Logging: Catch OperationCanceledException at the controller or middleware level and return a 499 or 200 rather than logging as an unhandled exception β€” client disconnects are not application errors
  • Background services: Always propagate the shutdown token; never use CancellationToken.None in long-running loops
  • Code review gate: Require explicit justification in code review when CancellationToken.None appears in request-scoped handlers

FAQ

Should every controller action parameter include CancellationToken? Yes for actions that perform I/O β€” database reads, external API calls, file operations. The framework injects HttpContext.RequestAborted automatically when you add the parameter. For actions that only do in-memory computation, it adds no value but also no harm.

What happens if I don't handle OperationCanceledException? Unhandled OperationCanceledException propagates up to the framework and typically results in a 500 response if not caught, or a connection reset if the socket is already closed. More importantly, it appears in your error logs as an exception, inflating your error rate metrics with noise that isn't a real application error. Catch it at the middleware or filter level and handle it as a cancelled request.

Can I use CancellationToken with Dapper? Yes. Most Dapper extension methods accept an optional CancellationToken parameter. The same governance rules apply β€” propagate for reads, evaluate carefully for writes.

How do I enforce a timeout without relying on the client to disconnect? Use CancellationTokenSource with a timeout: new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token. Link it with HttpContext.RequestAborted using CancellationTokenSource.CreateLinkedTokenSource to cancel on whichever fires first β€” client disconnect or timeout.

Does cancelling a token roll back an EF Core transaction? No. Cancellation does not automatically roll back a database transaction. If your code was inside an explicit transaction and the token fires during an awaitable operation inside that transaction, EF Core throws OperationCanceledException. Your code must catch it and explicitly roll back the transaction. The token is a cooperative signal, not a transactional boundary.

Should background services use the request token or the shutdown token? Always the shutdown token (IHostedService.StartAsync and BackgroundService.ExecuteAsync receive the shutdown token). Never use a request-scoped token in a background service β€” request tokens cancel when the HTTP request ends, which would stop your background work prematurely.

More from this blog

C

Coding Droplets

119 posts