Slow CI pipelines are often blamed on:
- Heavy test suites
- Complex integrations
- Rust compilation time
But in many cases, the real issue is much simpler:
- Your tests are not fully using the CPU available in the CI runner.
- But in many cases, the real issue is much simpler:
- Your tests are not fully using the CPU available in the CI runner.
Step 1 — Understand How cargo test Uses Threads
Rust’s test harness runs tests in parallel by default. However, in CI environments:
- CPU limits may restrict available cores
- Containers may expose fewer threads
- The harness may default to 1 thread in constrained setups
- You should never assume your CI is using all available CPUs.
Instead, verify it.
Step 2 — Check How Many CPUs Your Runner Has
Inside your CI job, run:
nproc
Example output:
2
This means the environment has 2 logical CPUs available. If you don’t explicitly configure thread usage, your tests might not use both.
Step 3 — Explicitly Set --test-threads
script:
- cargo test -p my_crate module_a
- cargo test -p my_crate module_b
- cargo test -p my_crate module_c
- cargo test -p my_crate module_d
Each invocation runs sequentially. To ensure each test run uses all available CPU cores, capture the number of CPUs dynamically:
script:
- THREADS=$(nproc)
- echo "Running tests with ${THREADS} threads"
- cargo test -p my_crate module_a -- --test-threads=${THREADS}
- cargo test -p my_crate module_b -- --test-threads=${THREADS}
- cargo test -p my_crate module_c -- --test-threads=${THREADS}
- cargo test -p my_crate module_d -- --test-threads=${THREADS}
Why the Double Dash (--) Is Important. The -- separator is critical. Everything before -- is interpreted by cargo. Everything after -- is passed to the test binary (Rust’s test harness).
--test-threads is a test harness argument — not a cargo argument. If you forget the separator, the flag won’t work.
Step 4 — Why Use $(nproc) Instead of a Fixed Number?
You could hardcode:
--test-threads=2
But that creates a hidden maintenance issue. If the runner changes from 2 CPUs to 4, your CI won’t scale automatically.
Using: THREADS=$(nproc) ensures:
- Automatic adaptation
- No future edits required
- Better portability between environments
Step 5 — Make Sure Your Tests Are Safe to Parallelize
Parallel test execution requires test isolation. Your tests should:
- Avoid global mutable state
- Avoid shared in-memory singletons
- Avoid reusing the same database instance
- Avoid mutating global environment variables
A safe pattern is to instantiate dependencies per test:
fn create_test_repository() -> InMemoryRepository {
InMemoryRepository::new()
}
[tokio::test]
fn example_test() {
let repo = create_test_repository();
// test logic here
}
Each test gets its own isolated state. When You Should Disable Parallelism. If a test suite depends on shared external state (for example, a real database instance), you may need to force sequential execution:
cargo test -- --test-threads=1
Apply this only to specific test groups that require it. Do not disable parallelism globally unless necessary.
Optional Optimization: Avoid Repeating Expensive Setup
Sometimes slow tests are caused by repeated expensive operations (for example, hashing, cryptographic setup, or large fixture generation). You can cache computed values safely using OnceLock:
use std::sync::OnceLock;
static PRECOMPUTED_VALUE: OnceLock<String> = OnceLock::new();
fn get_precomputed_value() -> String {
PRECOMPUTED_VALUE
.get_or_init(|| {
expensive_operation()
})
.clone()
}
fn expensive_operation() -> String {
// Simulate heavy work
"computed_result".to_string()
}
This ensures:
- The expensive operation runs only once
- Tests remain deterministic
- No unsafe global mutation occurs
However, always evaluate tradeoffs:
- Does it significantly reduce runtime?
- Does it add unnecessary complexity?
- Is parallelism alone sufficient?
Often, proper thread configuration already solves most CI performance issues.
Expected Impact
If your CI was running tests effectively single-threaded on a multi-core runner, explicitly configuring --test-threads can:
- Reduce test stage time dramatically
- Improve resource utilization
- Avoid unnecessary infrastructure upgrades
In many cases, improvements of 2–3x are realistic.
Final Checklist
If your Rust CI feels slow, verify the following:
- How many CPUs does the runner expose? (nproc)
- Are tests running in parallel?
- Is --test-threads explicitly configured?
- Are tests properly isolated?
- Are expensive operations unnecessarily repeated?
Before rewriting your test suite or scaling infrastructure, make sure you are actually using the hardware available to you.
Top comments (0)