Back to blog

Async Rust: Lessons from the Trenches

·5 min read

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
async fn bad_example() { std::thread::sleep(Duration::from_secs(1)); // Blocks the executor! }

// 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

  • Batch database operations
  • Profile with tokio-console

    Async Rust is powerful but demands respect. Take time to understand the fundamentals.