Skip to content
Select theme

How to make several requests at once

Short answer: futures::future::join_all polls every future in the list at once and hands back the results in input order — but it starts all of them, so five requests mean five connections and five slots on the server. buffer_unordered(n), from the same crate, is the same idea with a cap on how many run at the same time.

Three crates. reqwest sends the requests, tokio drives them, and futures supplies the two combinators — reqwest doesn’t re-export them:

Cargo.toml
[dependencies]
reqwest = "0.13.5"
tokio = { version = "1.53.3", features = ["macros", "rt-multi-thread"] }
futures = "0.3.34"

Each block below runs against the same small server, written with nothing but std. /<i> sleeps for a fixed per-index delay, then answers 200 with the index as the body. /stats reports three numbers: how many requests were handled, the peak number in flight at once, and the order the responses were written. The delays are what make the overlap visible — the last URL answers first.

The baseline. Five requests, one .await at a time:

Run it with cargo run (cargo’s own Compiling and Finished lines are not shown here):

/0 -> 200 OK
/1 -> 200 OK
/2 -> 200 OK
/3 -> 200 OK
/4 -> 200 OK
handled=5 peak=1 done=0,1,2,3,4

peak=1 is the whole story: each response arrives before the next request is sent. The five delays add up to 2.7 seconds of waiting for 2.7 seconds of server time.

join_all takes a Vec of futures and polls them all on one task:

Run it with cargo run:

/0 -> 200 OK
/1 -> 200 OK
/2 -> 200 OK
/3 -> 200 OK
/4 -> 200 OK
handled=5 peak=5 done=4,3,2,1,0

peak=5: five connections were open at the same time, and the batch now takes as long as the slowest request — one second — instead of the sum. The done= line is the server’s view of that overlap: /4 finished first, because it has the shortest delay. join_all still returned the results in input order, /0 first. Futures are lazy, so collecting them into the Vec sends nothing; join_all is the thing that polls all five.

join_all has no cap. Give it a thousand URLs and it opens a thousand connections, and the server meets all of them at once. buffer_unordered(n) starts the first n and adds another as each one finishes:

Run it with cargo run:

bodies: ["1", "0", "2", "4", "3"]
handled=5 peak=2 done=1,0,2,4,3

peak=2 — never more than two requests in flight, and the batch takes 1.4 seconds rather than one. Note the order: buffer_unordered yields in completion order, so /1 comes back before /0, and the server’s done= line says the same. That order is a property of the delays, not a contract — under load two responses can swap places — so sort the results when input order matters, or stay with join_all when the list is small and bounded. join_all keeps the whole batch on one task; requests that need to be cancelled one by one belong in tokio::task::JoinSet instead.

The next article in this chapter reads the bodies as JSON.