Request Reply Pattern
.NET development
Cancellation
Asynchronous programming
Software architecture

How to implement cancellation in Request Reply Pattern in .NET?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Cancellation in a request-reply flow is trickier than cancellation in a local method call. In .NET, a CancellationToken can stop the caller from waiting, but in a distributed request-reply pattern you also need to decide what happens to the in-flight request, the pending reply registration, and possibly the remote worker that is still doing the work.

Separate Local Cancellation From Remote Cancellation

The first design decision is important: canceling the caller's wait is not automatically the same thing as canceling the remote operation.

There are two common meanings of "cancel":

  • Stop awaiting the reply locally and clean up the pending request entry
  • Send an explicit cancel message so the remote worker can stop if it has not finished

Many systems implement the first reliably and the second only when the business process really needs it.

Track Pending Replies by Correlation ID

A practical .NET implementation keeps a dictionary of pending requests keyed by a correlation ID. Each request gets a TaskCompletionSource that will be completed by the reply handler or by cancellation.

csharp
1using System.Collections.Concurrent;
2
3public sealed class PendingRequestStore<TReply>
4{
5    private readonly ConcurrentDictionary<Guid, TaskCompletionSource<TReply>> _pending = new();
6
7    public bool TryAdd(Guid id, TaskCompletionSource<TReply> source) =>
8        _pending.TryAdd(id, source);
9
10    public bool TryRemove(Guid id, out TaskCompletionSource<TReply>? source) =>
11        _pending.TryRemove(id, out source);
12}

This is the core structure that lets the request sender await a reply asynchronously without blocking a thread.

Register the Cancellation Token

When you send the request, register the token so cancellation removes the pending request and completes the task as canceled.

csharp
1public async Task<PriceReply> SendAsync(
2    PriceRequest request,
3    CancellationToken cancellationToken)
4{
5    var correlationId = Guid.NewGuid();
6    var tcs = new TaskCompletionSource<PriceReply>(
7        TaskCreationOptions.RunContinuationsAsynchronously);
8
9    if (!_pending.TryAdd(correlationId, tcs))
10        throw new InvalidOperationException("Duplicate correlation id.");
11
12    using var registration = cancellationToken.Register(() =>
13    {
14        if (_pending.TryRemove(correlationId, out var pending))
15        {
16            pending!.TrySetCanceled(cancellationToken);
17            _bus.Publish(new CancelPriceRequest(correlationId));
18        }
19    });
20
21    _bus.Publish(new PriceRequestEnvelope(correlationId, request));
22    return await tcs.Task.ConfigureAwait(false);
23}

A few details matter here:

  • 'RunContinuationsAsynchronously prevents reply handlers from running caller continuations inline'
  • The dictionary entry is removed on cancellation
  • An optional cancel message is sent to the remote side

If your system does not support remote cancellation, remove the cancel publish and just cancel the local wait.

Complete the Pending Request When the Reply Arrives

The reply handler looks up the correlation ID and completes the matching task.

csharp
1public void OnReplyReceived(PriceReplyEnvelope reply)
2{
3    if (_pending.TryRemove(reply.CorrelationId, out var pending))
4    {
5        pending!.TrySetResult(reply.Payload);
6    }
7}

This completion must be idempotent. If cancellation already removed the entry, the reply should be ignored or logged rather than treated as a failure.

Decide What the Remote Side Should Do

If you publish explicit cancel messages, the worker has to cooperate. That usually means the worker keeps its own map of active operations and observes a token or canceled-state flag during long-running work.

Without that extra design, cancellation remains local-only: the caller stops waiting, but the remote process still finishes and its reply is discarded when it returns.

That behavior is often acceptable for read-only queries, but it may be wasteful for expensive operations.

Common Pitfalls

  • Assuming CancellationToken alone cancels remote work is incorrect. It only cancels code that actually observes that token.
  • Forgetting to remove pending requests on cancellation creates memory leaks and eventual reply-routing bugs.
  • Treating a late reply as an error instead of a normal race condition makes distributed cancellation noisier than it needs to be.
  • Not deciding whether cancellation is local-only or protocol-level leaves the behavior ambiguous for callers and responders.

Summary

  • In request-reply systems, cancellation has both local waiting semantics and optional remote-stop semantics.
  • Track pending replies by correlation ID with a TaskCompletionSource.
  • Register the CancellationToken to remove the pending entry and cancel the awaiting task.
  • If the business flow requires it, send an explicit cancel message so the responder can cooperate too.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.