redrop

Why Drop Can't Be Async

Let's start slowly, with the actual problem, before we look at any solution.

In Rust, when a value goes out of scope, its Drop::drop method runs automatically:

struct Connection {
    // ...
}

impl Drop for Connection {
    fn drop(&mut self) {
        // clean up here
    }
}

This is one of Rust's nicest features — you don't have to remember to close things, the compiler does it for you, deterministically, at a well-defined point.

The trouble is that fn drop(&mut self) is an ordinary, synchronous function. It cannot contain .await. So if closing your resource properly requires talking to something asynchronously — sending a "goodbye" message over a socket, flushing a batch of buffered log lines to a remote collector, releasing a lease held in a database — you're stuck. There is no async fn drop.

The usual workarounds, and why they're not quite enough

A few patterns show up in real codebases to work around this:

And there's a second problem hiding underneath all three: none of them account for what happens when many values need this kind of cleanup at once — say, a burst of a few thousand connections closing during a deploy. Spawn all of them unbounded, and you can hammer whatever they're talking to. Block on each one in turn, and cleanup that should take milliseconds takes minutes.

Prior art

redrop isn't the first attempt at this. A few existing crates tackle the same problem:

These all solve the single-value case reasonably well. What they don't address is the second problem above: many drops firing at once, with no shared queue, no backpressure, and no way to know cleanup actually finished before the process exits. redrop takes the same starting point — you still can't await inside drop, so cleanup still has to be handed off — but routes every #[async_drop] through one shared collector, so a burst of drops is queued and drained under a bounded concurrency limit instead of spawned unbounded, and redrop::shutdown() gives you an explicit .await point to guarantee the queue is drained before exit.

Rust itself is working on closing this gap at the language level, too. Nightly has an experimental AsyncDrop trait behind the #![feature(async_drop)] gate, tracked in rust-lang/rust#126482 and described by the async fundamentals initiative's async drop roadmap. It's the real, compiler-integrated version of what this crate is approximating — an actual async fn drop. It's also unstable, with no stabilization date, and existing code needs something that works on stable Rust today. redrop is meant as a bridge: a proc-macro #[async_drop] that gets you most of the ergonomics now, so that migrating to the language feature later — if and when it stabilizes with a similar shape — should mean deleting the macro, not rewriting the cleanup logic.

What we actually want

We want something that:

  1. Lets us write the cleanup as a normal async fn, close to the type it belongs to.
  2. Runs automatically when the value is dropped — no explicit call to remember.
  3. Doesn't block the thread that triggered the drop.
  4. Handles a burst of many drops without either losing them or overwhelming whatever they talk to.

That's what the rest of this tutorial builds, using redrop's #[async_drop] macro.

Setting Up →