Async Best Practices
Rules for responsive async Rust services - non-blocking I/O, structured concurrency, and honest runtime boundaries.
How to Use This List
- Apply before shipping Axum/Tokio services to production.
- Use as a PR rubric for new async endpoints and background workers.
- Pair with load tests and
tokio-consolewhen latency regresses.
A - Runtime & Structure
- Libraries expose
async fn; binaries own#[tokio::main]. One runtime per process. - Prefer native async drivers (tokio, sqlx, reqwest). Avoid
spawn_blockingas default. - Tune
worker_threadsafter measurement. More threads is not free scalability. - Use
current_threadonly for tests or!Send+LocalSet. Servers usemulti_thread. - Document MSRV and runtime choice in README. Onboarding depends on it.
B - Non-Blocking Discipline
- Never
std::thread::sleepor sync I/O on worker threads. Usetokio::time::sleepandtokio::fs. - Offload CPU and legacy SDKs with
spawn_blocking. Cap concurrency with semaphores. - Never hold
std::sync::MutexGuardacross.await. Usetokio::sync::Mutexor short scopes. - Wrap Rayon/
par_iterinspawn_blocking. Do not block workers on parallel sections. - Set client timeouts at connect, request, and idle layers. Hung peers stall resources.
C - Tasks & Concurrency
- Prefer
join!for independent awaits on one task.spawnwhen lifetimes or CPU isolation need it. - Use
select!loops with cancel-safe branches. Document which methods are cancel-safe. - Propagate shutdown via
CancellationTokenorwatch. Do not rely on process kill alone. - Await or abort every
JoinHandle. No leaked background tasks after errors. - Limit fan-out with semaphores. Protect peers, FDs, and DB pool slots.
D - State & Types
- Share app state with
Arc+ cheap clones. DB pool, HTTP client, config snapshots. - Prefer message passing (channels) over
Arc<Mutex<App>>. Actors for complex mutable state. - Ensure spawned tasks are
Sendon multi-thread runtime. NoRcacross await in spawned work. - Use
async-traitor native async traits intentionally. Know dyn vs generic trade-offs. - Validate errors at boundaries with typed
Result. Map to stable HTTP/problem responses.
E - Operations
- Instrument with
tracingspans per request and task. Include trace ids across await. - Load-test graceful shutdown and client disconnect. Handlers cancel cooperatively.
- Monitor blocking pool queue depth. Regression signal for sync calls in async paths.
- Test with
#[tokio::test]andtime::pause. Deterministic timeout tests. - Revisit sync islands when deps ship async. Remove executor bridges when possible.
FAQs
Is async always faster?
No - only when many concurrent I/O waits share few threads. CPU-bound or low concurrency may favor sync threads.
Biggest production mistake?
Blocking worker threads with sync I/O or CPU loops - causes tail latency spikes under load.
Axum + sqlx pattern?
Arc<PgPool> in state, async handlers, timeouts on queries, migrations offline.
When to spawn vs join?
join! for coordinated parallel awaits; spawn for fire-and-forget or long-lived workers with JoinHandle tracking.
Client disconnect?
Dropped handler future cancels work - make side effects idempotent or move critical commits outside cancellable section.
Single-threaded async?
Valid for embedded or !Send futures - not typical for multi-core API servers.
Error handling?
thiserror in libraries, anyhow in binaries; never swallow errors in spawned tasks.
Backpressure?
Bounded channels, semaphores on outbound calls, HTTP 503 when overloaded - unbounded queues hide failure until OOM.
Testing strategy?
Unit test pure logic sync; integration test handlers with reqwest against axum::Router in #[tokio::test].
First refactor for legacy sync?
Identify top 3 blocking calls in hot path; replace with async or spawn_blocking with limit.
Related
- Async Basics - getting started
- Blocking in Async - offload guide
- Cancellation & Timeouts - deadlines
- Concurrency Best Practices - thread rules
- Tokio Best Practices - runtime tuning
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.