Laszlo

Hello, I am Laszlo

Software-Engineer, .NET developer

Contact Me

Cancellation and Application Shutdown in an HTTP Server

I work on a personal project to build an ASP.NET Core server to serves HTTP/2 and HTTP/3 request-response pairs. This server integrates with ASP.NET Core applications, providing an alternative to Kestrel, IIS, HTTP.sys, and similar servers.

In most codebases I've encountered, significant effort goes into optimizing the startup process—starting the host, registering types, initializing background services, and establishing downstream connections. The goal is usually to achieve the fastest and most efficient application initialization. However, it's much less common for applications to pay the same attention to the shutdown process. That’s the focus of this post.

For server applications, there's often an assumption that requests are retriable. If a request is interrupted due to application shutdown, the client can simply retry. However, HTTP only guarantees safe retries for idempotent verbs and compliant servers.

At the infrastructure level, sophisticated hosting solutions can drain all active HTTP connections before terminating the server process. In a less controlled environments, a process might be terminated abruptly, without draining connections or even sending a SIGINT (CTRL+C) or SIGTERM signal.

In .NET, developers can use the UseConsoleLifetime method to handle SIGINT and SIGTERM. In a typical ASP.NET Core application (as of .NET 10), the application host manages the app lifecycle, including these signals, out of the box.

StopAsync and CancellationTokens

The .NET class library provides a cooperative cancellation mechanism using CancellationToken and CancellationTokenSource. The CancellationToken and CancellationTokenSource types are at the core of this mechanism. Most types that perform long-running operations offer overload methods that accept a cancellation token parameter. Typical application code only needs to pass such tokens to the 'leaf' operations that implement cancellation for the underlying mechanisms: for example, sockets, timers, file operations, etc.

At a higher level, a Host can register types implementing IHostedService, which provides hooks to start and stop services in sync with the host’s lifecycle.

ASP.NET Core servers use a custom interface, IServer, with StartAsync and StopAsync methods. Unlike IHostedService, IServer's StartAsync includes an IHttpApplication<TContext> application parameter. Both interfaces accept a CancellationToken. When StopAsync is called, it signals the application to begin shutdown. The service may attempt to finish ongoing work gracefully, but the caller might not want to wait indefinitely. If a timeout is reached, the cancellation token is triggered, signaling that shutdown should proceed more forcefully. Even then, because cancellation is cooperative, the handler can still try to reach a safe state for critical actions.

The key distinction is that StopAsync is awaitable, so the caller knows when the operation completes.

Shutdown Hierarchy and Reasons in Servers

In an ASP.NET Core application, a server may need to stop or cancel operations at several conceptual levels. Let’s look at the stream, connection, server, and host levels, with examples of why shutdown might occur at each:

  • Request/Stream: A request can be cancelled. In HTTP/2, a RST_STREAM frame cancels a stream; in HTTP/3, the QUIC protocol manages stream lifecycles. Streams can be aborted at any time by the QUIC implementation, which in .NET means handling a QuicException. The server might also terminate a stream for protocol violations. Closing a stream doesn’t affect other connections or the server as a whole.

  • Connection: Connections can be terminated gracefully using GOAWAY frames in HTTP/2 and HTTP/3, but abrupt termination is also possible (e.g., TCP or QUIC connection closure). This can be initiated by the client or an intermediary. In .NET, a terminated QUIC connection also results in a QuicException. When a connection ends, all its streams are closed, and the application should be notified that responses are no longer needed. Ideally, the server sends GOAWAY frames for graceful shutdown, but often connections end abruptly due to errors. Notably, HTTP/2 and HTTP/3 differ in how they communicate GOAWAY and error reasons.

  • A Server may decide to shut down. In this case, the server term refers to the entity that establishes a connection and listener pair for the incoming requests. Termination from the server is probably the least frequent. A more common case is when a server cannot open the requested port, which prevents it from starting. A server may need to terminate because the underlying TCP or QUIC listener resource is terminated (e.g., by the OS). In such cases there isn't much to do for a server other than closing the ongoing connections and streams and freeing up their resources.

  • Host: The host can be terminated gracefully (e.g., via SIGTERM) or abruptly. With graceful termination, the host signals the server to stop accepting new requests. The server can then choose a shutdown strategy: close all connections immediately, or close idle ones first and wait for active streams to finish.

This hierarchy encourages combining StopAsync and CancellationToken. Using linked cancellation tokens, a server can cascade cancellation across different levels. Linking is also useful when multiple sources can trigger cancellation. Meanwhile, StopAsync allows async operations to complete before returning.

Typical Challenges

  • Concurrent shutdown reasons: What if multiple shutdowns are triggered at once? How should the application handle a new, more urgent shutdown reason while another is already in progress?

  • Handling cancellation in .NET: Methods can check a token’s IsCancellationRequested property or call ThrowIfCancellationRequested, which throws an exception and results in a cancelled Task. When awaiting such tasks, the call site must handle these exceptions appropriately.

Summary

By giving equal attention to both startup and shutdown processes, you can build more robust and reliable server applications. Leveraging .NET’s cancellation mechanisms and understanding the shutdown hierarchy helps ensure graceful and predictable application behavior—even in complex or abrupt termination scenarios.