1.2.5 Test Sharding

recommended

shard_count is how you speed up one oversized test target without immediately refactoring it into several new labels. Bazel still sees one target, but when sharding is enabled it launches the test runner once per shard and tells each invocation which slice it owns through TEST_TOTAL_SHARDS and TEST_SHARD_INDEX1.

One test label can start several runner processes
Bazel creates the shard invocations. The test runner decides which cases each invocation runs.
ONE TEST TARGET
//:sharded_test
one Bazel label shard_count = 3
Bazel launches the same test executable three times.
↓ THREE SEPARATE PROCESSES ↓
SHARD 0
Runner process 1
TEST_TOTAL_SHARDS=3
TEST_SHARD_INDEX=0
Runner chooses cases alpha, delta
SHARD 1
Runner process 2
TEST_TOTAL_SHARDS=3
TEST_SHARD_INDEX=1
Runner chooses cases beta, epsilon
SHARD 2
Runner process 3
TEST_TOTAL_SHARDS=3
TEST_SHARD_INDEX=2
Runner chooses cases gamma, zeta
Runner contract: read the total and index, run only that shard's cases, and create or touch the file named by TEST_SHARD_STATUS_FILE.
Sharding splits the work inside one test target. It does not split a list of Bazel labels across CI workers.

One Target, Many Invocations

Sharding is about one target, not about a whole bazel test //... run. If //integration:api_test is one slow, heavy target, shard_count = 4 tells Bazel to launch that same runner four times and let each invocation select a different subset of test cases1. That makes shard_count the right tool when the bottleneck is one oversized suite, not poor target selection in CI.

The minimal test-sharding snippet shows the contract directly:

load("@rules_shell//shell:sh_test.bzl", "sh_test")

sh_test(
    name = "sharded_test",
    srcs = ["sharded_test.sh"],
    shard_count = 3,
    size = "small",
)

shard_count does not partition test cases by itself. It sets the number of shard invocations, and Bazel launches the runner once for each shard. The runner performs the actual partitioning by reading TEST_TOTAL_SHARDS and TEST_SHARD_INDEX and choosing its own slice of the suite1.

think

Predict: Your runner reads TEST_TOTAL_SHARDS and TEST_SHARD_INDEX and faithfully runs only its own slice of the cases. The logs even show shard 0 and shard 1 choosing different tests. Is that enough for Bazel to accept the sharded run?

Reveal

Not yet. Correct partitioning is invisible to Bazel unless the runner also creates or updates the file named by TEST_SHARD_STATUS_FILE. That file is the runner's handshake: "I understood the sharding contract for this invocation."

Without it, Bazel treats the runner as not sharding-aware and fails the sharded test, even if the runner happened to split the cases perfectly. The implementation checklist is therefore two-part: choose the shard's cases, then touch the shard status file before exiting.

The Runner Must Opt In

Runner support for sharding requires more than reading the environment variables. A sharding-aware runner must also create or update the file named by TEST_SHARD_STATUS_FILE. If it does not, Bazel treats the runner as non-sharding-aware and fails the sharded test1.

In sharded_test.sh, the runner touches TEST_SHARD_STATUS_FILE and then distributes six named cases with a simple index % total split. One real run of bazel test //:sharded_test --test_output=all --cache_test_results=no produced:

shard=1/3
cases=beta epsilon

shard=2/3
cases=gamma zeta

shard=0/3
cases=alpha delta

Shard indexes are zero-based, and each shard gets its own process and subset of work1. Log order is incidental. The contract specifies which cases each shard receives rather than which shard prints first in one particular run.

extra

Runner Support Is Framework-Specific

Bazel defines only the sharding contract. The boundary of "what exactly can be split" depends on the runner or test framework behind the rule. The limits are framework-specific: in Go with rules_go, sharding applies to test functions, not subtests, so subtests still need t.Parallel() if they should run concurrently within one shard2.

Frontend setups can look different. There the runner may already know how to shard, and Bazel provides the shard index and total count. Robinhood's frontend setup is a concrete real-world example of that shape: for Playwright, Bazel passes shard information through environment variables into the framework config instead of teaching Bazel the framework's internal splitting logic3.

Test Sharding Is Not CI Target Sharding

A common CI pattern uses bazel query to expand a large list of test targets and distribute those labels across parallel CI jobs4. That is target sharding. shard_count operates within one target and splits its internal suite across multiple runner invocations1,4.

In large repositories, you often combine both. The pipeline first distributes labels across workers, then especially heavy targets keep sharding inside each worker. The broader scheduling side of that story returns later in 5.2 Query and H.7.2 Evidence Portfolio4.

bazel run Is The Wrong Lens

If you try to debug such a test through bazel run //:my_test, one important limit applies: Bazel only emulates the test environment there, and multi-shard tests do not work faithfully in that mode. If you really need to launch the test binary directly without sharding, disable it for that invocation with --test_sharding_strategy=disabled5.

shard_count speeds up one test target while leaving its label set and the normal tools for target analysis, logs, and CI planning unchanged. When one specific shard starts failing, the next step is still ordinary log and output inspection from 1.2.7 Test Output & Debugging.

key takeaway

Sharding is a tool for rescuing one oversized test target. Bazel launches the runner multiple times, but it does not know how to partition test cases on its own: the runner must read TEST_TOTAL_SHARDS and TEST_SHARD_INDEX, then confirm support through TEST_SHARD_STATUS_FILE. Without that handshake, extra processes do not produce a real suite split.

Check your understanding · 2 questions

1.Match each sharding variable to its role:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
TEST_TOTAL_SHARDS
TEST_SHARD_INDEX
TEST_SHARD_STATUS_FILE

2.When is sharding the appropriate tool?

Select one answer

0 of 2 answered

Footnotes

  1. Test encyclopedia — the formal sharding contract: shard_count, one runner invocation per shard, TEST_TOTAL_SHARDS, TEST_SHARD_INDEX, and the TEST_SHARD_STATUS_FILE requirement 1 2 3 4 5 6

  2. Managing Flaky Tests With Bazel and rules_go - Zhongpeng Lin & Yushan Lin, Uber — practical use of shard_count for oversized targets and the Go-specific limit that sharding applies to test functions, not subtests

  3. How Robinhood uses Bazel to tame frontend complexity at scale — Playwright receives shard information from Bazel through environment variables instead of implementing the split inside Bazel itself

  4. How Bazel Uses Buildkite for CI — the pattern of sharding a list of targets with bazel query, which is a different layer of parallelism than shard_count 1 2 3

  5. Commands and Optionsbazel run on test targets only emulates the test environment and does not faithfully support multi-shard tests. --test_sharding_strategy=disabled is the workaround