Async Rust Patterns I Use Every Day

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.