agentleFS
d-oitd-oit/rust-2026-template/.agents/skills/tokio-performance/SKILL.md

tokio-performance

Skill15 starsChanged 5 months agoMITView on GitHub

Audit and design Tokio async Rust systems for throughput, latency, fairness, contention, blocking, and bounded concurrency. Use for Tokio runtime design, async performance reviews, task scheduling, spawn/spawn_blocking decisions, locks, channels, I/O, and tail-latency problems.

The file

Show its rules and text

We split this skill into the rules it gives an agent

15 rules. About 0 are common, 3 are distinctive, and 12 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 12

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Cooperative Yielding: Use yield_now() only when measurements or loop structure demonstrate worker thread starvation in repeatedly-ready …

    line 49
  2. Very specific:

    Keep lock scopes minimal and never hold locks across unrelated .await calls.

    line 57
  3. Very specific:

    tokio::task::yield_now(): Use yield_now() deliberately only when measurements or tight loop structure demonstrate starvation.

    line 64
  4. Very specific:

    Batching: Use batching (e.g., buffer_unordered, batch channel receives) when it reduces scheduling, syscall, or task coordination overhead.

    line 65
  5. Distinctive:

    Always default to bounded designs for network, filesystem, database, external API calls, or worker queues:

    line 69
  6. Very specific:

    Bounded Channels: Prefer tokio::sync::mpsc::channel(capacity) over unbounded channels to enforce upstream backpressure.

    line 70
  7. Very specific:

    Bounded Fan-out: Use tokio::sync::Semaphore, futures::stream::StreamExt::buffer_unordered, or fixed worker pools rather than unbounded …

    line 71
  8. Distinctive:

    Prefer synchronization mechanisms in this order:

    line 76
  9. Very specific:

    Immutable Snapshots: Use Arc<T>, ArcSwap, or Copy-On-Write (std::sync::RwLock<Arc<T>>) for read-heavy shared state or registries.

    line 78
  10. Very specific:

    Short Synchronous Locks: Use std::sync::Mutex or parking_lot::Mutex for extremely short data-only updates (e.g., updating a counter or …

    line 79
  11. Very specific:

    Async Locks: Use tokio::sync::Mutex or tokio::sync::RwLock ONLY when access across .await points is genuinely required or when waiting for …

    line 80
  12. Very specific:

    Deterministic rows are gates, performance rows are evidence: No CI check compares absolute timings across machines.

    line 118
  13. Distinctive:

    Add a workload instead of arguing: If a trade-off is not represented, extend the harness first, then cite the new row in the discussion.

    line 127
  14. Very specific:

    triz-analysis: Use .agents/skills/triz-analysis/SKILL.md for evaluating throughput vs.

    line 168
  15. Very specific:

    anti-ai-slop: Use after performance refactors to keep code simple and readable.

    line 169

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Tokio Performance

Performance guidance for production Rust systems using Tokio. Optimize measured behavior from workload characteristics, not async code by reflex.

Core Rule

Measure first. Derive execution models, concurrency bounds, and lock strategies from workload characteristics.

Do not assume:

  • async is faster than synchronous code
  • spawn_blocking is always better for CPU work
  • tokio::sync::Mutex or RwLock is preferable to a synchronous lock or atomic/snapshot pattern
  • more tasks or more runtimes mean more throughput
  • yield_now() everywhere improves fairness

Workload Decision Matrix

Workload TypeCharacteristicsRecommended Execution ModelKey Considerations
Short Bounded CPUFast, non-blocking calculations (< 10–100 µs), memory parsing, small serializationsDirect Execution on Tokio WorkerOffloading to spawn_blocking introduces thread context switches and queue overhead that exceed the work duration.
Genuinely BlockingSynchronous disk I/O, blocking C/FFI calls, legacy blocking SDKs, blocking DB driversspawn_blocking or Dedicated Thread PoolPrevents blocking the Tokio worker thread event loop.
Long or Sustained CPUImage processing, heavy cryptography, compression, large payload parsing (> 100 µs - ms+)spawn_blocking or Dedicated Worker PoolProtects Tokio worker threads from starvation. For sustained heavy CPU, prefer a dedicated Rayon/thread pool to avoid exhausting Tokio's blocking pool.
Batchable OperationsHigh-rate small I/O operations, frequent channel sends, small database updatesBatching & Micro-bufferingConsolidates syscalls, locks, and channel synchronization overhead.
Isolated SubsystemsMulti-tenant latency isolation, distinct thread priority requirementsWorkload IsolationIsolate via bounded semaphores or separate thread pools before considering a separate Tokio runtime.

Correctness vs. Performance Heuristics

Distinguish non-negotiable correctness rules from workload-driven performance heuristics:

Correctness Constraints (Mandatory)

  • Never hold lock guards across .await points or during I/O: Holding a std/tokio lock guard across .await leads to deadlocks, starvation, or sending non-Send guard types across thread boundaries.
  • Never perform blocking synchronous syscalls directly on Tokio worker threads: Blocking a worker thread starves all other tasks multiplexed on that worker thread.

Performance Heuristics (Workload-Driven)

  • spawn_blocking vs. Direct Execution: Balance context switch/queue overhead against worker thread yield time (~10–100 µs threshold).
  • Lock Type Selection: std::sync::Mutex (short data-only operations) vs. tokio::sync::Mutex (critical sections that MUST hold state across .await or await turn access).
  • Cooperative Yielding: Use yield_now() only when measurements or loop structure demonstrate worker thread starvation in repeatedly-ready loops.

Decision Protocol

  1. Identify the workload: I/O-bound, short CPU-bound, long CPU-bound, blocking sync API, bursty, or latency-sensitive.
  2. Define the target: throughput, median latency, tail latency (p95/p99), fairness, or resource limits.
  3. Distinguish correctness constraints from performance heuristics.
  4. Bound fan-out and queue growth by default when downstream capacity is finite.
  5. Keep lock scopes minimal and never hold locks across unrelated .await calls.
  6. Measure under representative load before and after changes.

Scheduling & Fairness

  • Avoid long synchronous sections inside async tasks.
  • Repeatedly-ready worker loops: Tasks that compute or poll continuously without hitting an I/O yield point can starve other tasks assigned to the same worker thread. Evaluate for scheduler starvation and tail-latency impact.
  • tokio::task::yield_now(): Use yield_now() deliberately only when measurements or tight loop structure demonstrate starvation. Do not insert it mechanically. Prefer Tokio's automatic cooperative budget (tokio::task::consume_budget) or event-driven channel backpressure.
  • Batching: Use batching (e.g., buffer_unordered, batch channel receives) when it reduces scheduling, syscall, or task coordination overhead.

Bounded Concurrency & Backpressure

Always default to bounded designs for network, filesystem, database, external API calls, or worker queues:

  • Bounded Channels: Prefer tokio::sync::mpsc::channel(capacity) over unbounded channels to enforce upstream backpressure.
  • Bounded Fan-out: Use tokio::sync::Semaphore, futures::stream::StreamExt::buffer_unordered, or fixed worker pools rather than unbounded tokio::spawn loops.
  • Timeouts & Shedding: Combine queue capacity limits with tokio::time::timeout and explicit load-shedding to prevent cascading failures under heavy load.

Synchronization & Lock Lifetimes

Prefer synchronization mechanisms in this order:

  1. Ownership and Message Passing: Move data via bounded channels (mpsc, oneshot).
  2. Immutable Snapshots: Use Arc<T>, ArcSwap, or Copy-On-Write (std::sync::RwLock<Arc<T>>) for read-heavy shared state or registries. Lookups clone an Arc pointer (~1-2 ns) and release the lock immediately before lookup and async execution. On mutation, create a cloned snapshot under a short write lock and swap the Arc.
  3. Short Synchronous Locks: Use std::sync::Mutex or parking_lot::Mutex for extremely short data-only updates (e.g., updating a counter or inserting into a HashMap). Release the lock immediately before any .await.
  4. Async Locks: Use tokio::sync::Mutex or tokio::sync::RwLock ONLY when access across .await points is genuinely required or when waiting for lock acquisition needs to be async non-blocking.

Pattern for state lookup and async execution:

lock -> lookup / clone snapshot -> drop lock -> await external work

Anti-Patterns & Counter-Examples

The five costliest Tokio misapplications — spawn_blocking and yield_now used reflexively, a second runtime, RwLock where Mutex or a snapshot fits, and a lock held across .await — are worked through with symptom, cost, and the correct alternative in references/counter-examples.md. Read it before accepting any of them.

Regression Harness (Preferred Evidence)

Performance claims in this repository are backed by the Tokio runtime harness. Reach for it before proposing an execution-model change, a yield_now(), a lock swap, or a second runtime:

HarnessWhat it answersNature
benchmarks/benches/tokio_runtime_bench.rs, benchmarks/benches/tokio_runtime_pressure_bench.rsexecution strategy (direct vs spawn_blocking), cooperative-yield cost, bounded vs unbounded fan-out, registry contention, service-queue latencyinformational telemetry, reports p50/p95/p99/max
benchmarks/tests/tokio_runtime_behavior.rsbounded-queue backpressure, repeatedly-ready starvation and its yield cure, spawn_blocking thread identity and deadlock-freedom, permit bounds, registry no-lost-updatedeterministic pass/fail, no timing thresholds
cargo bench -p benchmarks --bench tokio_runtime --bench tokio_runtime_pressure -- --quick   # full run: omit --quick
cargo nextest run -p benchmarks                              # deterministic invariants

Each benchmark group runs an instrumented sampling pass whose percentiles are printed in bencher format, so scripts/parse_bench.py records them as …/p50, …/p95, …/p99, …/max rows in benchmarks/events/** — the same machine-readable event stream as every other bench.

How to Interpret Results

  • Read tails, not averages. A change is only interesting when p95/p99 move outside run-to-run noise. Criterion's slope covers medians; the emitted percentiles cover tails.
  • Deterministic rows are gates, performance rows are evidence. No CI check compares absolute timings across machines; benchmarks/events/** is a time series for trend comparison, never a threshold.
  • Compare like with like. Same machine class, toolchain, and workload size. A laptop number does not transfer to production; a GitHub-hosted runner number does not either.
  • Change architecture only for the symptom you measured. p99 of direct CPU units that degrades as fan-out grows justifies spawn_blocking; a starved peer in the behavior test justifies yield_now() or batching at that specific loop; queue-depth growth justifies a bounded channel. A second runtime (Counter-Example 4) needs all of the above to be ruled out.
  • Add a workload instead of arguing. If a trade-off is not represented, extend the harness first, then cite the new row in the discussion.

Observability

Capture metrics to distinguish execution phase behavior:

  • task scheduling delay
  • task poll duration
  • queue depth and channel capacity
  • spawn_blocking pool usage
  • lock wait time and contention
  • I/O latency vs application work time

Review Checklist

  • Workload type (short CPU, blocking sync, long CPU, batchable, isolated) is explicitly identified.
  • Execution model (direct, spawn_blocking, dedicated pool) is chosen based on workload characteristics.
  • Correctness constraints (lock across await, blocking on worker thread) are respected.
  • No unbounded task fan-out (Semaphore, buffer_unordered, or bounded channel used).
  • Channels have deliberate capacity limits and backpressure.
  • Locks are data-only, short-lived, and never held across .await calls or external I/O.
  • Repeatedly-ready loops are evaluated for fairness and scheduler starvation without cargo-culting yield_now().
  • Claims of async performance improvements are backed by the regression harness (benchmarks/benches/tokio_runtime_bench.rs tails or benchmarks/tests/tokio_runtime_behavior.rs invariants), not by intuition.

Rationalizations

RationalizationReality
"Moving this small parse to spawn_blocking will make it faster."Thread context switching and queue overhead outweigh short CPU operations (<10-50µs). Run directly.
"Spawning an unbounded number of tasks is fine because Tokio is lightweight."Unbounded task creation exhausts file descriptors, network buffers, and memory. Use semaphores or bounded channels.
"A yield_now() loop fixes my starvation problem."Loops relying on yield_now() to prevent starvation indicate design flaws. Use event-driven notifications or channels.

Red Flags

  • Holding std::sync::Mutex locks across .await points
  • Performing blocking synchronous filesystem or network I/O directly on Tokio worker threads
  • Unbounded tokio::spawn loops without backpressure or permits

Integration & Cross-References

  • AGENTS.md: Canonical project contract for coding agents.
  • agents-docs/conventions.md: Project-wide coding conventions and invariants.
  • triz-analysis: Use .agents/skills/triz-analysis/SKILL.md for evaluating throughput vs. latency, contention, and async vs. blocking trade-offs using TRIZ inventive principles.
  • anti-ai-slop: Use after performance refactors to keep code simple and readable.

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.