Tokio Best Practices
Operational rules for Tokio runtimes in production - worker sizing, blocking pool hygiene, and shutdown discipline.
How to Use This List
- Apply before first production deploy of a Tokio/Axum service.
- Revisit after latency incidents or Kubernetes rollout stalls.
- Pair with
tracing, load tests, and optionaltokio-console.
A - Runtime Configuration
- Use
multi_threadfor API servers.current_threadfor tests andLocalSetonly. - Set
worker_threadsfrom profiling, not guesses. Start at CPU count. - Tune
max_blocking_threadswhen usingspawn_blockingheavily. Monitor queue depth. - Enable
tracingandtokiofeatures you need only. Smaller binaries, faster builds. - One
Runtimeper process. Libraries do not embed#[tokio::main].
B - Task & I/O Hygiene
- Spawn per-connection or use Axum - do not block accept loop. Throughput dies on serial accept.
- Name tasks in hot paths for debugging.
task::Builder::new().name(...). - Await or abort every
JoinHandle. Track background work inJoinSet. - Use
BufReader/BufWriteron socket I/O. Reduce syscall overhead. - Prefer
tokio::fsand async crates over std on workers. Blocking starves tasks.
C - Channels & select!
- Size
mpscchannels for burst backpressure. Not infinite queues. - Use
watchorCancellationTokenfor shutdown. Propagate to all loops. - Design
select!loops with cancel-safe branches. Document protocol reads. - Set
MissedTickBehavior::Skipon heartbeats. Avoid timer storms after pauses. - Layer timeouts on outbound I/O. Peers hang; resources leak.
D - State & Integrations
- Store
sqlx::Poolandreqwest::ClientinArcstate at startup. Reuse connections. - Use
tokio::sync::Mutexif mutex must cross await. Notstd::sync::Mutex. - Consider actor tasks for hot mutable state. Reduce lock contention.
- Close pool and flush logs on shutdown.
pool.close().awaitin graceful hook. - Propagate trace ids through spans and outbound headers. Correlate logs across services.
E - Operations
- Implement graceful shutdown (SIGTERM + drain timeout). Match K8s grace period.
- Monitor blocking pool saturation and task poll times. Early regression signal.
- Load-test deploy and SIGTERM paths. Hung shutdown blocks rollouts.
- Use
#[tokio::test(start_paused = true)]for timer tests. Deterministic CI. - Document runtime and Axum version in runbooks. Upgrades affect I/O and shutdown APIs.
FAQs
Default worker thread count?
Tokio defaults to CPU cores - adjust only with metrics showing worker starvation or excess idle threads.
When to spawn_blocking?
Legacy sync I/O, CPU batches, Rayon sections - always cap concurrency with semaphores when volume is high.
tokio-console worth it?
Valuable during incidents and tuning; optional in prod due to overhead - enable on canary nodes.
Axum without understanding Tokio?
Risky - handlers are Tokio tasks; blocking and shutdown behavior is runtime-owned.
feature = "full"?
Fine for learning; production crates should enable minimal feature set (rt-multi-thread, net, time, macros, etc.).
latency spikes?
First check blocking on workers, lock contention, and missing timeouts - not "async is slow."
connection limits?
Match Tokio concurrency to DB pool size and upstream rate limits - semaphores at boundaries.
upgrade Tokio?
Read changelog for I/O, timer, and MSRV changes; rerun shutdown and load tests.
single binary CLI?
#[tokio::main(flavor = "current_thread")] may suffice for one-shot async commands.
first production checklist item?
Graceful shutdown with traced drain and pool close - before optimizing microsecond wins.
Related
- Tokio Basics - getting started
- Graceful Shutdown - deploy safety
- Blocking in Async - blocking pool
- Async Best Practices - async-wide rules
- Tracing & Instrumentation - observability
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+.