redrop

Handling Load: Concurrency and the Background Daemon

Go back to chapter 1 for a moment: the reason a naive "just spawn a task per drop" approach falls short isn't just about losing work on shutdown — it's also about what happens when hundreds or thousands of values drop in a short burst. Think of a service restarting with a few thousand open connections, or a batch job finishing and tearing down a large pool of resources all at once.

redrop's collector is built around exactly this situation.

The queue and the pump

Every async_drop() future produced by the macro goes into a single, shared, unbounded queue. So far that's just deferring the problem — an unbounded queue can still grow without limit. What actually bounds the work is how the queue gets drained: a loop (redrop calls it the "pump") that keeps at most N cleanups running at once, where N is a number you control:

redrop::set_concurrency_limit(8);

Set it once, anywhere before (or even during) a burst of drops, and the pump respects it immediately — it re-checks the limit on every iteration, so you can even change it while cleanup is actively in progress. The default is 32, which is a reasonable starting point for a lot of workloads, but the right number really depends on what your async_drop() talks to: a handful of outbound HTTP calls tolerate more concurrency than, say, connections to a rate-limited database.

If you ran the showcase in the last chapter and saw overlapping flushes, that overlap was bounded by exactly this limit (8, by default, in showcase/redrop.toml — more on why it lives in that file rather than in the code in the next chapter).

Two ways the queue gets drained

This is where the tokio feature from chapter 2 matters.

Without it, nothing drains the queue on its own. Cleanup futures just accumulate until you call and .await redrop::shutdown() — at which point the pump runs, bounded by your concurrency limit, until everything queued (and everything that was already running) has finished. This is deliberately simple and works with any executor, not just Tokio.

With it, redrop additionally spawns a small background task — as soon as it gets a chance to (the first time an async_drop fires, or shutdown() is called, while inside a running Tokio runtime) — that keeps draining the queue continuously, at the same bounded concurrency, for as long as your program runs. Drops get cleaned up as they happen rather than piling up until you explicitly ask.

Either way, shutdown() still means the same thing: it's your guarantee that every queued and in-flight cleanup has actually finished. With the background daemon, calling it just closes the queue and waits for the daemon to finish draining what's left; without it, it drains the queue itself. Call it once, as the very last thing before your program exits — we'll cover exactly why "last" matters in the final chapter.

← Your First #[async_drop] Keeping Configuration Out of Your Code →