Rustup should check for updates concurrently

Jul 8, 2025

Rustup should check for updates concurrently

Back in 2023, it was noted that Rustup’s implementation could be surprisingly inefficient in certain situations, particularly for commands that intuitively should be concurrent. One such command is rustup check, which verifies which installed toolchains have updates available. This command is particularly useful for developers which have a lot of channels to check. Although Rustup is often used in CI environments, these don’t target a sufficient number of channels for this change to be noticeable. Future contributions targeting the concurrent download of components will have a much more meaningful impact on CI environments.

Making rustup check Concurrent

To speed things up, I developed a patch that changes how rustup check works internally, allowing it to perform toolchain checks concurrently. The core idea was to process all channels in parallel using a tokio_stream, limiting the number of in-flight futures to the number of channels being checked.

This alone provided a massive performance improvement, but of course, there were a few hiccups.

The Curious Case of Zero Channels

Although it doesn’t happen in typical usage, rustup check can be invoked in a setup where there are no channels to check. This edge case is rare but does appear in Rustup’s test suite, and it triggered a mysterious hang. After some digging (and with help from @rami3l), I realized that calling .buffered(0) or .buffer_unordered(0) causes a hang. This isn’t documented, but thankfully this comment on a futures-util PR pointed me in the right direction. The fix? Simply skip building the stream if the number of channels is zero. Simple enough, once you know what’s going on.

Showing Progress without Glitches

Once the concurrency issue was solved, another challenge appeared: how to show progress to the user as the toolchains are checked. One naive approach would be to wait until all checks finish, then report, but that would introduce a noticeable delay and degrade the user experience. Instead, we used the indicatif crate to display real-time progress bars while preserving the correct output order. This required implementing the TermLike trait for our custom terminal abstraction. The implementation was fairly straightforward, though we did run into some visual glitches. Occasionally, indicatif would try to move the cursor up by 0 lines, which broke the formatting. Once again, the fix was simple: avoid trying to move the cursor when n == 0.

Is it any Faster?

To evaluate the performance impact of these changes, a series of benchmarks were ran using hyperfine. The setup included a 50 Mbps internet connection, 16 GB of RAM, and an i7-1165G7 processor. The modified version of the rustup check was compared against the current implementation on the master branch, where the benchmark was executed 100 times, with 5 warmup runs beforehand. To ensure a meaningful comparison, 20 different toolchains were checked for updates in each run. The results showed an impressive 3.3× speedup over the current implementation. This exceeded then initial expectations, thanks to the realization that, since indicatif already handles the output in the correct order, we could safely replace buffered with buffer_unordered. It is important to note that this change doesn’t introduce any overhead to Rustup (kudos to @Kobzol for pointing this out). Even with a small number of channels to check (e.g., just 1), there is still a performance improvement (around 1.2×), though it’s relatively modest compared to scenarios with more channels.

See it in Action

To give a better understanding of what this change implied regarding the user experience, I leave below a short animation. On the left, you’ll see the current (sequential) behavior, and on the right, the new (concurrent) behavior:

Showcase of the new rustup check command