Rust + Tokio Async Patterns
Tokio fundamentals: tasks, select!, channels, cancellation, join!.
# CLAUDE.md — Rust + Tokio Async
## Setup
- `#[tokio::main]` on `main`. For libraries, accept a runtime handle from the caller — never assume one.
- Pin the runtime version in `Cargo.toml`. Major versions break compatibility.
- Default to the multi-thread runtime. Use the current-thread runtime only when you need single-thread guarantees.
## Tasks
- `tokio::spawn(async move { ... })` schedules a task on the runtime. Returns a `JoinHandle<T>`.
- Don't fire-and-forget unless you genuinely don't care. Hold the handle and `await` it, or `abort` it intentionally.
- Use `tokio::task::JoinSet` for fan-out:
```rust
let mut set = JoinSet::new();
for item in items { set.spawn(work(item)); }
while let Some(res) = set.join_next().await { ... }
```
## Cancellation
- Cancellation is cooperative. Tasks are dropped at the next `await` point.
- Use `tokio::select!` to race futures, including a cancellation token:
```rust
tokio::select! {
res = work() => handle(res),
_ = cancel.cancelled() => break,
}
```
- `tokio_util::sync::CancellationToken` for structured cancellation across many tasks.
## Channels
- `tokio::sync::mpsc` (multi-producer, single-consumer) — most common.
- `tokio::sync::broadcast` for fan-out to multiple subscribers.
- `tokio::sync::oneshot` for "one-shot" reply patterns (request → response).
- `watch` for "current value" semantics where late receivers should see the latest.
## Sync primitives
- `tokio::sync::Mutex` — async-aware. `std::sync::Mutex` is fine when held briefly and not across `.await`.
- `RwLock` only when reads vastly outnumber writes. Otherwise `Mutex` is simpler.
- Don't hold any lock across `.await`. Either drop it explicitly (`drop(guard)`) or restructure.
## Timing
- `tokio::time::sleep` and `interval` for delays. Never `std::thread::sleep` in async — it blocks the runtime.
- `tokio::time::timeout(duration, fut)` to bound any await.
- `Instant::now` is fine in async code; the deltas are correct.
## Blocking work
- CPU-bound or sync I/O work goes in `tokio::task::spawn_blocking`. Returns a `JoinHandle<T>`.
- The blocking pool has a separate thread budget. Don't dump unlimited blocking work — it'll stall.
- For "blocking" libraries you can't avoid, wrap them in a small `spawn_blocking` shim.
## Streams
- `tokio_stream` and `futures::Stream` for async iteration.
- `StreamExt::buffered(n)` to bound concurrency in pipelines.
- Don't collect a stream into a `Vec` if you don't have to. Keep it streaming.
## Don't
- Don't call `.block_on(...)` from inside an async function. That's how you create deadlocks.
- Don't share a tokio Mutex across `.await` if a `parking_lot::Mutex` would do — `parking_lot` is faster but synchronous.
- Don't swallow `JoinError`. It carries panic info.
- Don't use `Rc` or `RefCell` across tasks. They're not `Send`. Use `Arc` + `Mutex`.
Other Rust templates
Modern Rust Rules
Idiomatic Rust: ownership, error handling with ?, traits, modules, cargo.
Rust + Axum Web Server
Axum routers, extractors, middleware, and structured error responses.
Rust CLI Tools (clap)
Build polished CLIs with clap derive, anyhow, and structured logging.
Rust Testing Patterns
Unit tests, integration tests, doctests, proptest, and CI patterns.