26.4Graceful shutdown

Last updated June 13, 2026

The server works and handles many connections at once (lesson 26.3). One thing is still missing, and it's the kind of thing that separates a toy from a real program: it never stops cleanly. Hit Ctrl-C and the process dies instantly, abandoning any requests the workers are mid-way through and never properly winding down the threads. This final lesson adds graceful shutdown: when the pool goes away, it finishes outstanding work and joins every worker before exiting. The tool for it is Drop (chapter 21), and using it here is the perfect closing note, the whole course converging on one small, correct destructor.

The goal: drop the pool, and it tidies up

We want this to be automatic. When the ThreadPool is dropped, at the end of main, or whenever it goes out of scope, it should: stop accepting new work, let each worker finish its current job, and wait for all of them to actually end. That's RAII (lesson 21.4): tie the cleanup to the value's lifetime, so "the pool went away" means "the workers were shut down properly." No manual shutdown call to forget; dropping the pool is the shutdown.

Two problems to solve

Implementing Drop for the pool means solving two things.

First, the workers loop forever. Each worker's thread sits in loop { ... recv() ... }, blocking on the channel, waiting for jobs. To make a worker stop, we have to make its recv() return an Err, which happens exactly when the channel's sender is dropped (lesson 22.2). Recall the worker code: on Err, it breaks out of the loop and the thread ends. So shutting down is really "drop the sender, and every worker's recv returns Err, and they all break."

Second, we must wait for the workers to finish. Dropping the sender tells the workers to stop, but they might be mid-job. We need to join each worker's thread (lesson 22.1) so the pool's drop doesn't return until every worker has actually completed and exited.

The trouble: join consumes the JoinHandle (it takes ownership), but the handle is a field inside the Worker struct, which we only have by &mut inside drop. The clean fix is to make the thread handle an Option, so we can take() it out, replacing it with None, and own it for the join.

The implementation

Make the sender an Option too (so we can drop it), and the worker's thread an Option<JoinHandle>:

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,   // Option so Drop can take it
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,   // Option so Drop can take it
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        // Step 1: drop the sender, closing the channel.
        drop(self.sender.take());

        // Step 2: join every worker so they finish before we return.
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Read the two steps. self.sender.take() pulls the sender out of its Option (leaving None) and drop immediately destroys it, closing the channel. Now every worker's recv() returns Err, so each worker breaks out of its loop and its thread starts winding down. Then the loop over &mut self.workers joins each one: worker.thread.take() owns the handle out of its Option, and thread.join() blocks until that worker's thread has fully finished. Because step 1 happens before the join loop, the workers have already been told to stop, so the joins complete promptly instead of hanging forever. (The worker's new and the execute method need tiny tweaks to wrap their values in Some and to send through self.sender.as_ref().unwrap(); the shape above is the heart of it.)

With this Drop in place, the pool cleans up after itself automatically. To see it, you can make main accept only a couple of requests and then return (for example, for stream in listener.incoming().take(2)), so the pool drops at the end of main:

Shutting down worker 0
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

Each worker is joined in turn; the program exits only after all four threads have ended. No abandoned work, no leaked threads. The server shuts down gracefully, because dropping the pool is shutting it down.

Drop ties it together

This is RAII completing the course's arc. The pool acquires resources (threads) when it's created and releases them (joins them) when it's dropped, so resource lifetime tracks value lifetime, the exact pattern from lesson 21.4. There's no separate "remember to shut down" step a caller could forget; the language guarantees the destructor runs. The server's correctness, finishing work and reclaiming threads, is enforced by the same Drop machinery that frees a String's buffer. Your last lesson's code rests on your first lessons' ideas.

You built a web server

Take a moment. You have written, in safe Rust with no dependencies, a multithreaded HTTP server: it listens on a socket, parses requests, routes them, serves files, handles many connections concurrently through a thread pool you implemented yourself, and shuts down gracefully. That is a real systems program, the kind that, in many languages, requires a framework and a great deal of care to get right, and you built it from parts you understand down to the metal. Every chapter showed up: networking and files (20), iterators and closures (19), structs and error handling (10, 12), threads and channels and locks (22), smart pointers and Drop (21), trait objects and generics and lifetimes (16, 15, 17). Nothing was filler.

Where to go from here

This is the end of the course's lessons, but not the end of your Rust. The appendices that follow are reference material for when you need them: a tour of essential crates and tools, the details of Rust editions, and a closing section on where to go next, contributing to a crate, reading deeper, and the projects worth building to keep growing. Turn there when you're ready. You came in (lesson 0.1) not knowing what a variable was. You're leaving having built a concurrent web server in one of the most demanding languages in wide use. That's no small thing. Thank you for working through all of it, and welcome to Rust.

Quiz time

Question #1

To shut the pool down, why do we drop the sender first?

Show solution

The workers loop forever, blocking on recv() for new jobs. A recv() only returns Err (signaling the worker to break out and stop) when the channel's sender is dropped (all senders gone). So dropping the pool's sender closes the channel, causing every worker's recv() to return Err, which makes each worker exit its loop. Without dropping the sender, the workers would wait forever and the join step would hang.

Question #2

Why are the worker's thread field and the pool's sender field wrapped in Option?

Show solution

Because Drop only has &mut self, but joining a thread (thread.join()) and dropping the sender require owning those values, not just borrowing them. Wrapping them in Option lets drop call .take(), which moves the value out and leaves None behind, giving ownership needed for join/drop while satisfying the borrow checker. It's the idiomatic way to move a value out of a struct you only have by mutable reference.

Question #3

How does implementing Drop for ThreadPool make shutdown "graceful," and why is that better than a manual shutdown method?

Show solution

The Drop impl closes the channel (telling workers to stop) and then joins every worker, so the pool's destruction doesn't complete until all workers have finished their current jobs and their threads have ended, no abandoned work, no leaked threads. It's better than a manual shutdown method because it's automatic and impossible to forget: the cleanup is tied to the pool's lifetime (RAII), so dropping the pool is shutting it down, with the language guaranteeing the destructor runs.

That is the end of the course. You've gone from your first println! to a multithreaded web server, and every tool in between is now yours. The appendices are there when you want them; the rest is the code you'll write next. Go build something.