Async Rust: Lessons from the Trenches
Async Rust has a steep learning curve. Here's what I wish I knew when starting.
The Mental Model
Async in Rust is different from JavaScript or Python. Key concepts:
- Futures are lazy - they don't run until polled
- The runtime (Tokio, async-std) drives the execution
- Lifetimes still matter (and get more complex)
Common Pitfalls
1. Blocking in Async Context
// DON'T do this
// DO this instead async fn good_example() { tokio::time::sleep(Duration::from_secs(1)).await; }
2. Spawning Without Bounds
Always limit concurrent operations:
use futures::stream::StreamExt;let results = futures::stream::iter(urls)
.map(|url| fetch(url))
.buffer_unordered(10) // Max 10 concurrent
.collect::>()
.await;
Patterns That Work
Structured Concurrency
Use JoinSet for managing multiple tasks:
let mut set = JoinSet::new();for url in urls {
set.spawn(fetch(url));
}
while let Some(result) = set.join_next().await {
handle_result(result);
}
Graceful Shutdown
Always plan for shutdown:
tokio::select! {
_ = server.run() => {},
_ = shutdown_signal() => {
server.graceful_shutdown().await;
}
}
Performance Tips
1. Use channels for communication between tasks
Async Rust is powerful but demands respect. Take time to understand the fundamentals.