> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testdino.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Distribute Playwright tests across machines with swarm mode

> Spread one Playwright test suite across N CI machines at runtime with tdpw swarm, and merge every machine's results into one TestDino test run.

Swarm mode spreads one Playwright test suite across N continuous integration (CI) machines while the test run is in progress. Each machine takes the next spec file when it frees up, and TestDino merges every machine's results into one test run with per-machine results.

<Note>
  Swarm mode requires `@testdino/playwright` 2.10.0 or later. Earlier versions exit with `error: unknown command 'swarm'`.
</Note>

<Note>
  Swarm mode is available on the Team and Enterprise plans. On other plans, starting a swarm exits with `Swarm is available on the Team and Enterprise plans. Upgrade the plan to run a swarm.`
</Note>

## Quick Reference

| Topic | Link | Best for |
| :- | :- | :- |
| Compare distributions | [Pick swarm mode or a fixed partition](#pick-swarm-mode-or-a-fixed-partition) | Choosing between sharding, split mode, and swarm mode |
| Run a swarm | [Start a swarm and join it](#start-a-swarm-and-join-it) | Working example |
| CI setup | [Configure a CI matrix](#configure-a-ci-matrix) | GitHub Actions config |
| Machine names | [Set a stable machine ID](#set-a-stable-machine-id) | Attributing test cases to a machine |
| Options | [CLI flags](#cli-flags) | Flags, environment variables, and exit codes |
| Live progress | [Watch a swarm while it runs](#watch-a-swarm-while-it-runs) | Active Test Runs |
| Results | [Read the Machines panel](#read-the-machines-panel) | Per-machine results on the run detail |
| Scoping | [Filter by machine](#filter-by-machine) | Detailed Analysis for 1 machine |
| Coverage gaps | [When a spec runs nowhere](#when-a-spec-runs-nowhere) | Incomplete test runs |
| Errors | [Troubleshooting](#troubleshooting) | Error messages and fixes |

## Pick swarm mode or a fixed partition

All 3 spread one test run across parallel CI jobs. They differ in when the partition is decided.

| Behavior | Sharding | Split mode | Swarm mode |
| :- | :- | :- | :- |
| Partitioned | Before the run, by Playwright | Before the run, by you | During the run, by TestDino |
| Flag | `--shard=1/4` | `--split 1/4` with `--split-id` | `tdpw swarm` |
| A slow machine | Keeps its whole share | Keeps its whole share | Receives fewer specs |
| Every job runs | The same command | A different command per job | The same command |
| Grouped by | `--ci-run-id` | `--split-id` | The swarm ID |
| Run detail section | **Shards** | **Splits** | **Machines** |

Sharding and split mode fix each machine's workload before the first test case runs, so one slow machine holds the whole test run open. Swarm mode hands out the next spec file when a machine frees up, so a slow machine ends up with less to do.

The unit of distribution is 1 spec file per Playwright project. A test suite whose runtime sits in a single long spec file gains little from swarm mode: that file always runs whole on 1 machine.

Swarm mode does not combine with sharding or split mode.

## Start a swarm and join it

Swarm mode is 1 command in 2 modes. `--machines` starts a swarm and prints its ID. `--id` joins one.

Start the swarm once per pipeline, before the matrix fans out:

```bash theme={null}
npx tdpw swarm --machines 4 --out swarm.json
```

`--out` writes the started swarm as JSON with a `swarmId` key, which the matrix legs read. Set `workers` to a whole number in your Playwright config so TestDino can balance the test suite.

Then on every machine, pass the same ID:

```bash theme={null}
npx tdpw swarm --id "$SWARM_ID" --machine-id runner-1
```

Each machine connects, runs the batches of spec files it is handed, and exits when the swarm ends. Every machine reports under the same test run.

Passing both `--machines` and `--id` is an error, and so is passing neither.

## Configure a CI matrix

The start step runs once and publishes the swarm ID. Each matrix leg joins with that ID and its own machine ID.

```yaml theme={null}
jobs:
  swarm:
    runs-on: ubuntu-latest
    outputs:
      id: ${{ steps.start.outputs.id }}
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - id: start
        env:
          TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
        run: |
          npx tdpw swarm --machines 4 --out swarm.json
          echo "id=$(jq -r .swarmId swarm.json)" >> "$GITHUB_OUTPUT"

  test:
    needs: swarm
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        machine: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - env:
          TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
          TESTDINO_MACHINE_ID: m${{ matrix.machine }}
        run: npx tdpw swarm --id ${{ needs.swarm.outputs.id }}
```

Set `fail-fast: false` so a leg that fails, for example during `npm ci`, does not cancel the others.

A machine that never connects leaves its work to the others.

## Set a stable machine ID

The machine ID is what attributes a test case to a machine in the dashboard. It must stay the same for the life of the process.

TestDino resolves it from the first source that is set:

| Order | Source |
| :- | :- |
| 1 | `--machine-id <id>` |
| 2 | `TESTDINO_MACHINE_ID` |
| 3 | `CI_NODE_INDEX` or `CIRCLE_NODE_INDEX`, as `ci-<index>` |
| 4 | `<hostname>-<process ID>` |

GitHub Actions publishes no matrix index, so its legs fall through to hostname and process ID unless you set `TESTDINO_MACHINE_ID`. Set it: hostname and process ID change when a runner restarts, and the Machines panel then reports 1 machine as 2.

A short ID such as `m1` displays as **Machine 1**. Other IDs, such as `runner-1`, display as written.

## CLI flags

Swarm mode is its own command. Learn about the flags on `tdpw test` in the [Node.js CLI reference](/cli/testdino-playwright-nodejs#cli-flags).

| Flag | Environment variable | Description |
| :- | :- | :- |
| `--machines <N>` | `TESTDINO_MACHINES` | Start a swarm across N machines. Positive integer. |
| `--id <swarmId>` | - | Join the swarm with this ID instead of starting one. |
| `--machine-id <id>` | `TESTDINO_MACHINE_ID` | This machine's identity. Join only. |
| `--grep <pattern>` | - | Restrict the test suite to matching test cases. Start only, applied to every machine. |
| `--project <name...>` | - | Restrict the test suite to 1 or more Playwright projects. Start only. |
| `--out <file>` | - | Write the started swarm as JSON. Start only. |
| `--token <token>` | `TESTDINO_TOKEN` | API token. Required. |
| `--debug` | - | Verbose logging. |
| `-- <playwright args>` | - | Join only. Forwarded verbatim to every batch, for example `-- --workers=2`. |

Scope a swarm with `--grep` and `--project` when you start it.

After `--` on the joining machines, pass only run options such as `--workers` or `--retries`. A `--grep`, `--project`, `--shard`, or spec path there changes what every machine runs.

| Exit code | When |
| :- | :- |
| `0` | The swarm ended, including when test cases failed |
| `1` | The swarm could not start, the token is missing or rejected, or the machine lost its connection and could not reconnect |
| `130` | The job was cancelled while the machine was in the swarm |

A machine that loses its connection reconnects on its own.

## Watch a swarm while it runs

An in-progress swarm appears under Active Test Runs with a **SWARM** label. Expand the row for 1 tab per machine.

| Element | Shows |
| :- | :- |
| Progress line | Percent done, `69 / 109 completed` against the test suite size the swarm started with, and how many machines have reported |
| Machine tab | The machine and the count of test cases it has finished, for example `Machine 1 · 20` |
| Clock on a tab | `This machine finished its tests and is waiting for more` |
| `finishing up` | Every test case has reported and TestDino is closing the test run |

The number on a machine tab is finished work, not a share of the test suite.

## Read the Machines panel

A swarm run carries a **SWARM** badge in the Test Runs list. Its Summary tab shows a **Machines** section, titled with the machine count.

| Element | Shows |
| :- | :- |
| Machine row | Machine, status, timeline bar, duration, and test results |
| Timeline bar | How long the machine ran. A grey tail on a finished machine is time it sat idle while the longest machine was still running |
| `Imbalance` badge | How many times longer the slowest machine ran than the fastest. Shown on finished runs |
| Headline | 1 sentence naming what happened |

| Headline | When |
| :- | :- |
| `Running across 4 machines.` | The run is in progress. The count is machines that have reported so far, not the number you asked for |
| `Ran on a single machine.` | Only 1 machine reported |
| `Split evenly across 4 machines.` | No machine sat idle for a large share of the run |
| `Split across 4 machines. runner-3 finished 45s early.` | Names the machine that sat idle longest |

The `Imbalance` badge tooltip can name the cause. When the slowest machine ran a single spec file, splitting that file is the only way the run gets faster; adding machines does not help. When it ran several spec files, the work was packed unevenly.

## Filter by machine

Machine is a filter on the Summary tab and accepts multiple values.

| Action | Result |
| :- | :- |
| Click a machine row | Adds or removes that machine in the filter on Detailed Analysis |
| Click a machine chip on a test case row | Adds or removes that machine in the filter |
| `✕` on a `Machine:` chip | Clears that 1 value and leaves the rest applied |
| **Clear all** | Clears the machine filter. Status and search stay applied |

The filter is saved in the page URL, so a filtered view is shareable.

## When a spec runs nowhere

When a machine drops mid-run, another machine picks up its unfinished spec files. When no machine can run a spec file, the test run finalizes as **Incomplete**.

The Machines panel lists those files under `No machine could run`.

An incomplete test run is a coverage gap, not a failure: those test cases produced no result at all. Treat it as a red run.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Nothing to do: pass --machines <N> to start a swarm, or --id <swarmId> to join one.">
    The command ran with neither flag. `--machines` starts a swarm, `--id` joins one. A joining step that reads its ID from a previous job usually hits this when that job failed and the output is empty.
  </Accordion>

  <Accordion title="Pass either --machines (start a swarm) or --id (join one), not both.">
    A joining step still carries the starting step's `--machines`, or `TESTDINO_MACHINES` is set for the whole workflow. Remove both from the matrix legs.
  </Accordion>

  <Accordion title="--id needs the swarm id to join.">
    `--id` was passed with an empty value, which almost always means the CI expression that supplies it resolved to nothing. Check the output name on the start job.
  </Accordion>

  <Accordion title="Unexpected argument">
    A spec path or other value was passed before `--`. Scope the test suite with `--grep` or `--project` when you start the swarm, and put Playwright arguments after `--` when you join.
  </Accordion>

  <Accordion title="workers not set to a positive integer in the Playwright config">
    The swarm still runs, but each machine gets 1 spec file at a time, so it takes longer. Set `workers` in your Playwright config to a whole number, not a percentage, so TestDino can balance the test suite.
  </Accordion>

  <Accordion title="The CI job passed but test cases failed">
    `tdpw swarm` exits `0` when the swarm ends, whatever the test results. Read the result from the test run in TestDino.
  </Accordion>

  <Accordion title="The Machines panel shows more machines than the matrix has legs">
    A machine ID changed mid-run, so TestDino counted 1 machine as 2. Set `TESTDINO_MACHINE_ID` to a value fixed by the matrix leg rather than letting it fall through to hostname and process ID.
  </Accordion>

  <Accordion title="The test run finalized as incomplete">
    At least 1 spec file could not run on any machine. The Machines panel names them. Run those specs locally: a spec that fails to load, rather than fails a test case, is the usual cause.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Node.js CLI reference" icon="terminal" href="/cli/testdino-playwright-nodejs">
    Every flag, environment variable, and config field on `tdpw`.
  </Card>

  <Card title="Split mode" icon="layer-group" href="/guides/playwright-split-mode">
    Merge jobs you partition by hand into one test run.
  </Card>

  <Card title="Test runs" icon="list-check" href="/platform/playwright-test-runs">
    Read the run detail, including the Machines breakdown.
  </Card>

  <Card title="CI optimization" icon="gauge-high" href="/guides/playwright-ci-optimization">
    Find the slow test cases worth distributing in the first place.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.