How can I run Node.js tests in parallel in CI?

Use Depot CI to split Node.js tests across parallel matrix jobs and balance each shard with historical JUnit timings. For smaller workflows, parallel steps can also overlap tests with linting and type checks inside one job.

Last updated 2026-09-09

The fastest way to run a large Node.js test suite in parallel is to split it across a matrix of Depot CI jobs, then use historical JUnit timings to keep the shards balanced.

Depot CI has no runner concurrency limit, so every shard can start at once. Each shard runs on its own runner with its own CPU and memory. Depot uses the duration of earlier test results to give every shard a similar amount of work. That keeps one unusually slow test file from deciding when the entire suite finishes.

There are three different ways to parallelize a Node.js workflow:

ApproachWhat runs concurrentlyBest fit
Parallel stepsIndependent commands inside one jobLint, type checking, builds, and shorter test suites
Matrix job shardingDifferent subsets of tests on separate runnersLarge suites that need more CPU than one runner can supply
Timing-balanced test splittingMatrix shards balanced using historical test timingSuites where test files have very different run times

A matrix creates the parallel jobs. It doesn't decide which tests belong in each job. Depot's test splitting handles that assignment and updates it as the suite changes.

Split Vitest across four jobs

This Depot CI workflow discovers Vitest files, creates four jobs, assigns a non-overlapping subset to each job, runs the subset, and uploads a JUnit report:

name: Node tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: depot-ubuntu-24.04
    strategy:
      fail-fast: false
      matrix:
        shard: [0, 1, 2, 3]
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 10

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      - uses: depot/setup-action@v1

      - name: Run timing-balanced test shard
        uses: depot/tests-run-action@v1
        with:
          candidates-command: git ls-files | grep -E '\.(test|spec)\.(js|jsx|ts|tsx)$'
          candidate-type: classname
          command: >
            mkdir -p test-results &&
            xargs pnpm exec vitest run
            --reporter=default
            --reporter=junit
            --outputFile.junit=test-results/junit.xml
          report-path: test-results/junit.xml

DEPOT_MATRIX_JOB_INDEX and DEPOT_MATRIX_JOB_TOTAL tell the action which shard the current Depot CI job owns. You don't need to wire the matrix index into the action yourself.

The candidate command must print one runnable test file per line. Change it to match where your repository keeps its tests. The command receives the selected candidates on standard input, and xargs passes only that shard's files to Vitest.

Vitest accepts filenames as arguments but records them in the JUnit classname field. That's why this example sets candidate-type: classname. For Jest, Depot supports JUnit reports produced by jest-junit when its file field is enabled. In that setup, filename candidates are inferred correctly and candidate-type can be omitted.

JUnit history keeps the shards balanced

Splitting four test files into four jobs is easy. Splitting 2,000 files so that all four jobs finish at roughly the same time is the hard part.

A naive split by file count can produce three four-minute shards and one fourteen-minute shard. The workflow still takes fourteen minutes because the slowest shard is on the critical path.

depot/tests-run-action runs and reports each shard in one step. Every job uploads JUnit XML with the duration of its tests. On the next run, Depot uses those durations to build a new set of non-overlapping shards with similar predicted run times.

The first run doesn't need timing history. Filename candidates fall back to file-size splitting. Other candidate types use deterministic fallback weights. Because Vitest records these candidates as classnames, the example above uses deterministic weights on its first run. As JUnit results accumulate, later runs become timing-balanced.

The same reports also show up in Depot test results, where you can inspect failures across every shard and find slow or flaky tests across the organization.

Use parallel steps for independent Node.js checks

A matrix isn't the only way to remove sequential work. Depot CI can run independent steps concurrently inside one job:

jobs:
  checks:
    runs-on: depot-ubuntu-24.04
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile

      - parallel:
          - name: Lint
            run: pnpm lint
          - name: Type check
            run: pnpm type-check
          - name: Unit tests
            run: pnpm test

All three commands start from the same job state after dependency installation. Depot waits for the entire parallel group before continuing.

This removes unnecessary ordering, but it doesn't give each command a separate machine. Lint, type checking, and tests still share the runner's CPU and memory. If the test runner already uses every available core, running more work beside it can make all three commands slower. Use a matrix when the suite needs more aggregate compute.

Don't start several copies of the complete test suite in a parallel block. That repeats the same work. Test splitting creates disjoint shards so each test runs once.

Choose the number of shards from the critical path

Parallelism trades wall-clock time for more aggregate compute and repeated setup. Four five-minute shards can reduce a twenty-minute test step to roughly five minutes, but every shard still checks out the repository, restores caches, and installs dependencies.

Depot's unlimited concurrency means runner capacity doesn't force those shards to wait in line. It doesn't make extra jobs free. Increase the shard count while the slowest shard gets meaningfully shorter. Stop when setup time, contention, or one unsplittable test becomes the new critical path.

A useful progression is:

  1. Record the duration of the test step on one Depot CI runner.
  2. Split the suite into two timing-balanced jobs.
  3. Compare the slowest shard, total workflow time, and total compute.
  4. Move to four or eight shards only if the critical path keeps shrinking enough to justify the extra jobs.

Keep some suites sequential

Don't parallelize a test suite by default when:

  • The suite is shorter than the runner and dependency setup around it.
  • Tests depend on execution order or mutate shared global state.
  • Shards would fight over one database, port, account, queue, or external rate limit.
  • One integration test dominates the entire suite and can't be split further.
  • The Node.js test runner already saturates the runner and a larger runner is simpler than a matrix.
  • Failures are already intermittent. More concurrency can expose hidden shared-state bugs before you're ready to diagnose them.

Fix isolation before adding more shards. Give each job its own database schema, port range, temporary directory, and external resource names. Parallel tests are useful when the tests can actually run independently.

Start building with Depot
in minutes