Tokio is the executor. Axum, sqlx, reqwest, and WS run on it. HTTP: axum. Production/shutdown: rust-axum-production. WS hub: rust-axum-websocket. Language: rust.
Do not switch to async-std/smol in an Axum API. Multi-thread (#[tokio::main]) is the service default; current_thread only if the binary already is like that (CLI, test).
Non-negotiable rules
- Do not block workers: no heavy
std::thread::sleep,std::fs, long std mutex, synchronous DNS/waitin the handler. - CPU/FS/legacy sync →
spawn_blocking(or async crate). Network I/O → Tokio APIs/async. - Every task has an end:
JoinHandle,JoinSetor cancellation. Nospawnfire-and-forget of business work. select!with fair cancellation; the branch that loses does not leave orphaned I/O (useAbortHandle/ drop of the future /CancellationToken).Context/request deadline: propagate cancel, do not ignoreJoinHandlefrom the handler.
Boot
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// tracing, config, bind — rust-axum-production
Ok(())
}
- Runtime of
axum::serve+ graceful shutdown inmain, not a second runtime. - Cargo features:
rt-multi-thread,macros,net,sync,time— the ones the crate already requests. Do notfull“just in case” if the project is lean.
Tasks
| API | When |
|---|---|
tokio::spawn |
Async work tied to the runtime (fan-out, WS write loop) |
spawn_blocking |
Short CPU, diesel sync, zip, bcrypt |
JoinSet |
N tasks of the same unit; abort on drop |
LocalSet |
!Send — rare in an API; do not use in an Axum handler without reason |
'static+Sendin thespawnof the server. State:Arc.- Panic in task:
JoinError; do not swallow it.awaitoraborton shutdown.
select, time, cancel
tokio::time::timeout/sleep— neverthread::sleep.select!biased:biasedonly with an invariant. I/O timeouts on the client (reqwest) and on the server (towertimeout).tokio_util::sync::CancellationToken(or the project's) for the WS hub and workers. Dropping the token aborts children.
Channels
| Channel | Role |
|---|---|
mpsc |
One consumer (WS writer, internal queue) |
broadcast |
Fan-out (hub); lag → Lagged |
watch |
Last value (config, ready) |
oneshot |
One response |
Capacity limited on the hot path. unbounded only with backpressure elsewhere (rust-axum-websocket).
Anti-patterns
block_oninside an async handler (deadlock in the runtime)spawnper SQL query request (use the async pool)std::sync::Mutexheld across.await- Second
#[tokio::main]in a test lib without#[tokio::test] - Busy loop without
yield_now/ channel
Integration with other skills
- HTTP:
axum - WS:
rust-axum-websocket - Client:
reqwest - Logs:
tracing - Tests:
rust-unit-testing(#[tokio::test])
Completion criteria
- No blocking in the worker in new code
- Tasks with join/cancel on shutdown
- Channels with a bound or lag policy
timeoutTokio in the touched I/O- Single runtime aligned with Axum