As a Rust project grows, its test suite can become one of the slowest parts of the development process. A project that once completed its tests in a few seconds may eventually take several minutes or longer. This becomes a problem when every pull request must wait for the entire test suite to finish.
One way to reduce this delay is to run tests across multiple CI workers. However, splitting tests manually can introduce problems. Some workers may finish quickly while others remain busy, and configuring test filters for every module can become difficult to maintain.
Rust nextest helps solve this problem by running Rust tests through a dedicated test runner. It supports parallel test execution, test filtering, retries, and CI-oriented workflows. When combined with CI job matrices or other distribution mechanisms, it can help teams divide large test suites across multiple workers.
In this article, you will learn how Rust nextest works, how to configure it in a Rust project, and how to distribute tests across CI workers without making the pipeline unnecessarily complicated.
What Is Rust nextest?
cargo-nextest is an alternative test runner for Rust projects. It integrates with Cargo and provides features designed for running tests efficiently, particularly in continuous integration environments.
The standard Rust test workflow typically uses:
cargo test
This command compiles and runs tests using Rust's built-in test harness.
With nextest, you can use:
cargo nextest run
Nextest runs supported tests through its own execution and reporting workflow. It can schedule tests in parallel, apply filters, retry certain failures, and produce reports that are useful in CI pipelines.
However, parallel execution on one worker is not the same as distributing tests across multiple CI workers.
Nextest can run tests concurrently on a single machine. To distribute tests across separate machines or CI jobs, you need an additional distribution strategy, such as a CI matrix, a partitioning tool, or a workflow that assigns tests to different workers.
Understanding this distinction is important when designing a scalable testing pipeline.
Why Large Rust Test Suites Become Slow
A growing Rust codebase often contains several types of tests:
- Unit tests that validate individual functions and modules.
- Integration tests that exercise public interfaces.
- Database tests that require temporary databases or containers.
- End-to-end tests that communicate with external services.
- Regression tests that reproduce previously discovered bugs.
Not every test takes the same amount of time.
For example, a suite might contain 600 tests, with most completing quickly and a few taking several seconds each. If the runner executes too many tests sequentially, the overall execution time can increase.
Even when tests run in parallel, a single worker can become overloaded by CPU-intensive tests, memory-intensive processes, or tests that wait on external resources.
Consider this simplified example:
| Worker | Assigned tests | Estimated duration |
| Worker A | 150 | 2 minutes |
| Worker B | 150 | 3 minutes |
| Worker C | 150 | 7 minutes |
| Worker D | 150 | 2 minutes |
If the workers run simultaneously, the overall duration is approximately seven minutes, excluding setup and coordination overhead. Workers A, B, and D finish early but cannot reduce the time spent waiting for Worker C.
A more balanced distribution can reduce the time required for the entire test stage.
The challenge is to distribute work while keeping test execution reliable and easy to maintain.
Step 1: Install nextest
Start with an existing Rust project that uses Cargo.
You can install nextest using its Cargo installation package:
cargo install cargo-nextest --locked
The --locked option asks Cargo to use the package's committed lockfile when resolving dependencies for the installation.
Check that the command is available:
cargo nextest --version
Your CI environment must have a compatible Rust toolchain and the required dependencies installed. If your project uses native libraries, databases, or other system dependencies, configure those before running the tests.
For reproducible CI builds, pin the Rust toolchain and manage dependency versions consistently across workers.
Step 2: Run the Existing Test Suite
Before introducing distribution, establish a working nextest command.
Run:
cargo nextest run
Nextest discovers and runs supported tests in the selected Cargo workspace or package.
For a workspace containing multiple packages, you can run tests across the workspace:
cargo nextest run --workspace
You can also select a specific package:
cargo nextest run -p my-service
Replace my-service with the actual package name from your workspace.
Nextest supports test filters, so you can select tests by package, test name, or other supported criteria. Filters are useful for targeted test runs, but they should not be used as a substitute for a reliable distribution strategy.
Before distributing tests, make sure the full suite passes on one worker. This provides a baseline for checking whether the distributed pipeline runs all required tests.
Step 3: Understand Test Partitioning
Test partitioning divides a collection of tests into separate groups. Each group is assigned to a worker, and all workers execute their assigned groups concurrently.
For example, suppose a project has four CI workers:
- Worker 1 runs partition 1.
- Worker 2 runs partition 2.
- Worker 3 runs partition 3.
- Worker 4 runs partition 4.
Each worker must execute only its assigned tests, and the combined partitions must cover the intended test suite.
A correct partitioning system should satisfy three conditions:
- Every required test is assigned to at least one partition.
- No required test is accidentally omitted.
- Tests are distributed in a way that avoids unnecessary imbalance.
The third condition is about efficiency rather than correctness. Equal numbers of tests do not necessarily mean equal execution times.
For example, one partition might contain 100 fast unit tests while another contains 20 slow integration tests. The second partition may take much longer despite containing fewer tests.
A distribution tool that understands historical test durations can often balance work more effectively than a simple count-based split.
Step 4: Distribute Tests Using a CI Matrix
A CI matrix is one way to create multiple workers. The following example uses GitHub Actions to run four separate jobs.
This example assumes that your project has a partitioning mechanism that selects the correct tests for each worker. The exact command depends on the partitioning tool you choose.
name: Rust Tests
on:
pull_request:
push:
branches:
- main
jobs:
test:
name: Test partition ${{ matrix.partition }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
partition: [1, 2, 3, 4]
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Run assigned tests
run: |
echo "Run partition ${{ matrix.partition }} of 4"
# Invoke your partitioning mechanism here.
# It must select a distinct, complete portion
# of the test suite for this worker.
This is a workflow structure, not a complete test-distribution implementation. The final step deliberately leaves the partitioning command unspecified because a GitHub Actions matrix does not automatically divide tests, and nextest's ordinary run command does not by itself make separate CI jobs coordinate their test assignments.
To make the workflow operational, select a partitioning mechanism and configure the final step to invoke it with the current partition number and total number of partitions.
The example also uses version tags for its actions. For security-sensitive production pipelines, consider pinning third-party actions to reviewed commit SHAs and updating those pins through a controlled process.
Why Use fail-fast: false?
By default, matrix strategies can cancel other running jobs when a job fails. Setting fail-fast: false allows the remaining partitions to finish.
This can be useful for test suites because one failing partition should not necessarily prevent other workers from reporting their results. It also helps developers see more failures in a single CI run.
However, all required partitions must still be included in the workflow's final status. A successful job cannot compensate for a missing or failed partition.
Step 5: Choose a Partitioning Strategy
There are several ways to divide tests across CI workers. The right choice depends on the size of the suite and the complexity of your pipeline.
Strategy 1: Split Tests by Package
If your workspace has clearly separated packages, you can assign packages to different CI jobs.
For example:
cargo nextest run -p api-service
Another job might run:
cargo nextest run -p billing-service
This approach is easy to understand and maintain when package boundaries are meaningful.
However, it can produce an unbalanced pipeline if one package contains most of the slow tests. It also requires care when packages share integration tests or common setup requirements.
Strategy 2: Split Tests by Name or Filter
Nextest supports filtering tests, which can be useful when tests are organized into distinct groups.
For example, a team might separate a fast unit-test group from a slower integration-test group, provided the test names or metadata make the groups unambiguous.
This can work well for stable test categories, but manually maintained filters can become fragile. A newly added test might not match any group, or a test might match more than one group.
If you choose this approach, include a check that detects tests that are not assigned to any partition.
Strategy 3: Use a Dedicated Test Distributor
For large test suites, a dedicated distribution mechanism can assign individual tests or test binaries to workers.
A useful distributor may support historical execution times, automatic balancing, retries, and reporting. These capabilities help reduce the time spent waiting for the slowest worker.
Nextest can serve as the test runner within this architecture, while the distribution system determines which tests each worker should execute.
Before adopting a distributor, verify that it supports your CI provider, Cargo workspace layout, test filtering requirements, and desired reporting format.
Step 6: Keep the Results Reliable
Distributed testing introduces failure modes that are less common in a single-worker workflow.
For example, a test might pass alone but fail when another test modifies the same database. Another test might depend on a local file that exists on one worker but not another.
Use the following practices to keep distributed execution dependable.
Make Tests Independent
Each test should create and clean up the resources it needs. Avoid relying on execution order or shared mutable state.
When tests use databases, assign isolated schemas, databases, or records to each worker. Use unique resource names when multiple workers may access the same service.
Control External Dependencies
Tests that require network services can fail because of temporary outages, rate limits, or inconsistent test data.
Use controlled test environments where possible. If an external service is essential, make its availability and failure behavior explicit in the pipeline.
Handle Retries Carefully
Nextest supports retry configuration for failed tests. Retries can help identify transient failures, but they should not hide genuine defects.
Use retries selectively and keep the original failure visible in test reports. A test that passes only after repeated attempts may still indicate a race condition or unstable dependency.
Preserve Test Reports
Each worker should publish its test results, including failures and relevant logs.
Configure the CI workflow to collect reports even when a partition fails. Otherwise, debugging distributed failures can become unnecessarily difficult.
Common Mistakes to Avoid
Assuming a Matrix Automatically Splits Tests
A matrix creates separate jobs, but each job runs whatever command you configure. If every worker runs cargo nextest run without a filter or distributor, every worker may execute the entire test suite.
Dividing Tests Only by Count
Equal numbers of tests do not guarantee equal execution times. Use historical durations or measured timings when balancing becomes important.
Sharing Mutable Resources Across Workers
Separate CI machines may still access the same external database, cache, or cloud service. Shared state can create race conditions and inconsistent results.
Ignoring Build and Setup Time
Parallel test execution does not eliminate compilation, dependency installation, container startup, or environment setup. Measure the entire CI stage, not just the time spent executing tests.
Allowing Missing Partitions
If a matrix configuration changes or a worker fails to start, some tests may never run. Make the final CI result depend on every required partition and validate the coverage of your distribution mechanism.
Advantages and Disadvantages
Advantages
- Parallel execution can reduce the time required for large test suites.
- CI matrices make it straightforward to create independent workers.
- Nextest provides test filtering, retries, and useful reporting.
- Package-based distribution can be simple to maintain.
- Dedicated distributors can improve load balancing for large suites.
Disadvantages
- Additional CI workers increase infrastructure cost.
- Compilation and environment setup may be repeated across jobs.
- Poor partitioning can leave some workers idle while others remain busy.
- Shared resources and order-dependent tests can cause failures.
- Distribution requires extra configuration and validation.
Summary
Rust nextest provides a test runner designed for efficient Rust test execution, with features that are useful in CI pipelines. It can run tests concurrently on one machine, but distributing a test suite across multiple CI workers requires an additional mechanism to assign tests to each worker.
For smaller projects, splitting tests by package may be sufficient. For larger suites, a dedicated distributor that balances tests using historical execution times can provide better results.
Start by making the complete test suite reliable on a single worker. Then introduce partitioning, verify that every required test runs, and compare total pipeline duration before and after the change.
The goal is not simply to run more jobs. It is to shorten feedback time while preserving complete test coverage, clear failure reports, and dependable builds.
Join the conversation! Your thoughts help the community grow.