Async Rust Patterns I Use Every Day
Table of Contents
Practical patterns for writing reliable async Rust code with Tokio.
Working on high-throughput backend services, I’ve developed a set of async patterns that help me write reliable, performant code. Here are the ones I reach for most often.
Graceful Shutdown
Every production service needs to shut down cleanly. Here’s my go-to pattern:
use tokio::signal;
use tokio::sync::broadcast;
async fn run_server() -> anyhow::Result<()> {
let (shutdown_tx, _) = broadcast::channel::<()>(1);
// Spawn worker tasks
let worker = {
let mut shutdown_rx = shutdown_tx.subscribe();
tokio::spawn(async move {
loop {
tokio::select! {
_ = shutdown_rx.recv() => break,
_ = do_work() => {}
}
}
})
};
// Wait for shutdown signal
signal::ctrl_c().await?;
drop(shutdown_tx); // Signal all workers
worker.await?;
Ok(())
}Bounded Concurrency
When processing many items, you often want to limit concurrency:
use futures::stream::{self, StreamExt};
async fn process_all(items: Vec<Item>) {
stream::iter(items)
.map(|item| async move {
process_item(item).await
})
.buffer_unordered(10) // Max 10 concurrent
.collect::<Vec<_>>()
.await;
}Retry with Exponential Backoff
Network calls fail. Always have a retry strategy:
use std::time::Duration;
use tokio::time::sleep;
async fn fetch_with_retry<T, F, Fut>(
mut f: F,
max_retries: u32,
) -> anyhow::Result<T>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = anyhow::Result<T>>,
{
let mut delay = Duration::from_millis(100);
for attempt in 0..max_retries {
match f().await {
Ok(result) => return Ok(result),
Err(e) if attempt == max_retries - 1 => return Err(e),
Err(_) => {
sleep(delay).await;
delay *= 2;
}
}
}
unreachable!()
}Channel-Based Actor Pattern
For stateful components, I use a channel-based actor:
use tokio::sync::{mpsc, oneshot};
enum Command {
Get { key: String, resp: oneshot::Sender<Option<Value>> },
Set { key: String, value: Value },
}
async fn cache_actor(mut rx: mpsc::Receiver<Command>) {
let mut cache = HashMap::new();
while let Some(cmd) = rx.recv().await {
match cmd {
Command::Get { key, resp } => {
let _ = resp.send(cache.get(&key).cloned());
}
Command::Set { key, value } => {
cache.insert(key, value);
}
}
}
}Timeouts Everywhere
Never let async operations hang indefinitely:
use tokio::time::timeout;
use std::time::Duration;
async fn fetch_data() -> anyhow::Result<Data> {
timeout(Duration::from_secs(30), do_fetch())
.await
.map_err(|_| anyhow::anyhow!("Request timed out"))?
}Conclusion
These patterns have served me well across multiple production systems. The key insight is that async Rust gives you fine-grained control — use it to build robust, predictable systems.
What patterns do you find yourself reaching for? I’d love to hear about them.