Introduction
Async iterators (also called streams) yield values asynchronously — each element may require awaiting an I/O operation, a timer, or another async task. Combined with async closures, they enable functional-style data processing over asynchronous data sources like network connections, message queues, and file streams.
Key Concepts
Streamtrait: The async equivalent ofIterator. Defined infutures/tokio-streamastrait Stream { fn poll_next(...) -> Poll<Option<Self::Item>>; }.async-iterator(nightly): The standard library is working onAsyncIteratoras the official async iterator trait. For stable Rust, use thefutures::Streamtrait.- Stream combinators: Async versions of
map,filter,fold, and others that apply async transformations to streams.
Real World Context
Async iterators power real-time data processing: reading lines from a TCP socket, processing messages from a Kafka consumer, watching filesystem changes, or streaming database query results row by row. They combine the efficiency of async I/O with the expressiveness of iterator chains.
Deep Dive
Creating a stream from an async closure
The futures::stream module provides utilities to create streams:
rustuse futures::stream::{self, StreamExt}; // Create a stream that yields values from an async computation let page_stream = stream::unfold(1u32, |page| async move { if page > 5 { None // Stop after 5 pages } else { let data = fetch_page(page).await; Some((data, page + 1)) // (yielded_value, next_state) } });
unfold is the async equivalent of building an iterator with a state machine. The async closure receives the current state and returns Option<(Item, NextState)>.
Stream combinators with async closures
Stream combinators work like iterator adapters but support async operations:
rustuse futures::stream::{self, StreamExt}; let user_ids = stream::iter(vec![1, 2, 3, 4, 5]); // map with an async operation let users = user_ids.then(|id| async move { fetch_user(id).await }); // filter with an async predicate let active_users = users.filter(|user| async { user.is_active().await }); // collect all results let result: Vec<User> = active_users.collect().await;
The .then() method applies an async function to each element, and .filter() accepts an async predicate.
Buffered concurrent processing
Streams support concurrent processing with backpressure control:
rustuse futures::stream::{self, StreamExt}; let urls = stream::iter(vec![ "https://api.example.com/users", "https://api.example.com/posts", "https://api.example.com/comments", ]); // Process up to 3 requests concurrently let responses: Vec<Response> = urls .map(|url| async move { reqwest::get(url).await.unwrap() }) .buffer_unordered(3) // Max 3 concurrent futures .collect() .await;
buffer_unordered(n) polls up to n futures concurrently, yielding results as they complete (not necessarily in order). Use buffered(n) for ordered results.
Combining async closures with streams
With Rust 1.85's async closures, you can write cleaner stream transformations:
rustuse futures::stream::StreamExt; // Create a processing pipeline using async closures let process_event = async |event: Event| -> ProcessedEvent { let enriched = enrich_event(event).await; validate_event(enriched).await }; let results: Vec<ProcessedEvent> = event_stream .then(process_event) .collect() .await;
The async closure captures the processing logic cleanly, and .then() applies it to each stream element.
Common Pitfalls
- Forgetting
.awaiton stream consumers —stream.collect()returns a future. You must.awaitit to drive the stream. - Unbounded concurrency — Using
buffer_unorderedwithout a limit can overwhelm downstream systems. Always specify a reasonable concurrency limit. - Mixing
IteratorandStream—stream::iter()converts a sync iterator into a stream. Do not confuseIterator::mapwithStreamExt::map.
Best Practices
- Use
buffer_unorderedfor I/O-bound work — It maximizes throughput by processing multiple items concurrently. - Prefer
stream::iterfor known collections — When the source data is already in memory,stream::iteris the simplest way to create a stream. - Handle errors in the stream — Use
.filter_map()or.try_for_each()to handle errors per-element rather than panicking.
Summary
- Async iterators (streams) yield values asynchronously.
futures::streamprovides stream creation and combinator utilities..then()applies an async function to each stream element.buffer_unordered(n)enables bounded concurrent processing.- Async closures (Rust 1.85+) make stream transformations concise and readable.
Code Examples
use futures::stream::{self, StreamExt};
// A practical async data pipeline:
// 1. Fetch user IDs from a paginated API
// 2. Fetch each user's details concurrently (max 5 at a time)
// 3. Filter to active users
// 4. Collect results
async fn get_active_users() -> Vec<User> {
let user_ids = stream::unfold(0u32, |page| async move {
let ids = fetch_user_ids_page(page).await;
if ids.is_empty() {
None
} else {
Some((stream::iter(ids), page + 1))
}
})
.flatten(); // Flatten stream of streams into single stream
user_ids
.map(|id| async move { fetch_user_detail(id).await })
.buffer_unordered(5) // Fetch 5 users concurrently
.filter(|user| futures::future::ready(user.is_active))
.collect()
.await
}
// Output: Vec of all active users, fetched efficiently with
// controlled concurrency and functional composition