rust 132 lines · 3 tabs

Graceful Shutdown for a Tokio TCP Server on Ctrl-C with a Broadcast Signal

Shared by codesnips Jul 2026
3 tabs
use tokio::sync::broadcast;

pub struct Shutdown {
    is_shutdown: bool,
    notify: broadcast::Receiver<()>,
}

impl Shutdown {
    pub fn new(notify: broadcast::Receiver<()>) -> Shutdown {
        Shutdown {
            is_shutdown: false,
            notify,
        }
    }

    pub fn is_shutdown(&self) -> bool {
        self.is_shutdown
    }

    pub async fn recv(&mut self) {
        if self.is_shutdown {
            return;
        }

        // A closed sender also counts as a shutdown request.
        let _ = self.notify.recv().await;
        self.is_shutdown = true;
    }
}
3 files · rust Explain with highlit

Graceful shutdown is the discipline of stopping a server without dropping in-flight work: the listener stops accepting new connections, existing connections are given a chance to finish, and the process exits only once everything has drained. This snippet wires that behavior into a tokio TCP server using a broadcast channel as a fan-out cancellation signal plus a TaskTracker to wait for outstanding tasks.

In shutdown.rs, the Shutdown type wraps a single-value broadcast::Receiver<()>. Every connection handler holds its own clone-derived receiver, and Shutdown::recv awaits the signal exactly once, latching is_shutdown so subsequent calls return immediately. A broadcast channel is chosen over a one-shot because the same shutdown notification must reach an arbitrary number of live connections simultaneously; sending on the paired Sender wakes them all. The notified flag guards against the common pitfall of awaiting an already-closed channel in a loop.

In server.rs, Listener::run owns the TcpListener, the broadcast::Sender used to hand out receivers, and a TaskTracker that records every spawned handler. The core is a tokio::select! that races listener.accept() against notify_shutdown in the handler; when a connection arrives, a new Handler is spawned onto the tracker with its own Shutdown::new(notify_shutdown.subscribe()). Each Handler::run again uses select! so a blocking read on the socket is interruptible — if the shutdown fires mid-read, the branch cancels the pending future and the handler returns cleanly rather than hanging.

In main.rs, signal::ctrl_c is awaited alongside the server via select!. When Ctrl-C lands, the server future is dropped (halting accept), then dropping the notify_shutdown sender broadcasts the signal, tracker.close() marks the tracker complete, and tracker.wait() blocks until every in-flight Handler finishes. This ordering matters: closing the tracker before all tasks are spawned would let wait() return early. The trade-off is that truly stuck handlers can still stall exit, so production code usually wraps the final wait() in a tokio::time::timeout to bound the drain window. This pattern generalizes to any accept-loop service where clean draining beats an abrupt process::exit.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
python
class Product(models.Model):
    name = models.CharField(max_length=200)
    slug = models.SlugField(blank=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    cost = models.DecimalField(max_digits=10, decimal_places=2)
    margin = models.DecimalField(max_digits=5, decimal_places=2, blank=True)

Django model signals vs overriding save

django python models
by Priya Sharma 2 tabs
rust
use crossbeam::channel::unbounded;
use std::thread;

fn main() {
    let (tx, rx) = unbounded();

Crossbeam for advanced concurrent data structures

rust concurrency lock-free
by Marcus Chen 1 tab
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

Channels (mpsc) for message passing between threads

rust concurrency channels
by Marcus Chen 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs

Share this code

Here's the card — post it anywhere.

Graceful Shutdown for a Tokio TCP Server on Ctrl-C with a Broadcast Signal — share card
Link copied