cypress-parallel: Run Cypress Tests in Parallel Without Cypress Cloud

cypress-parallel: Run Cypress Tests in Parallel Without Cypress Cloud

Cypress Cloud (formerly Dashboard) offers parallelization as a paid feature. The open-source cypress-parallel package delivers similar speed gains without the subscription. It distributes spec files across multiple CI workers using timing data from previous runs. Here's how to set it up.

What cypress-parallel Does

cypress-parallel reads your spec files, sorts them by recorded duration, and assigns them to N workers so each worker gets roughly equal total runtime. Unlike a simple alphabetical split, timing-based distribution minimizes the slowest worker's runtime.

It doesn't require a central server — each runner independently reads a shared weights file that you persist in CI cache.

Installation

npm install -D cypress-parallel

How It Works

# Run Cypress specs across 4 workers, this machine is worker 0
npx cypress-parallel \
  -s cy:run \          <span class="hljs-comment"># npm script that runs Cypress
  -t 4 \              <span class="hljs-comment"># total threads
  -m 0 \              <span class="hljs-comment"># this machine's index (0-3)
  -o <span class="hljs-string">'{"browser":"chrome"}'  <span class="hljs-comment"># extra Cypress options

The key flags:

  • -t — total number of parallel workers
  • -m — index of this worker (0-based)
  • -s — npm script that runs Cypress (usually cy:run or cypress:run)

After each run, cypress-parallel writes spec timing data to cypress/parallel-weights.json. Commit this file to keep timing data between runs.

package.json Setup

{
  "scripts": {
    "cy:run": "cypress run",
    "cy:parallel": "cypress-parallel -s cy:run -t 4 -m"
  }
}

Call it with the worker index appended: npm run cy:parallel 0, npm run cy:parallel 1, etc.

GitHub Actions Matrix Setup

name: Cypress Tests

on: [push, pull_request]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        index: [0, 1, 2, 3]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Restore Cypress binary cache
        uses: actions/cache@v3
        with:
          path: ~/.cache/Cypress
          key: cypress-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

      - name: Install Cypress binary
        run: npx cypress install

      - name: Start dev server
        run: npm run start &
        # Or use cypress-wait-on: npx wait-on http://localhost:3000

      - name: Run Cypress shard ${{ matrix.index }}/4
        run: npx cypress-parallel -s cy:run -t 4 -m ${{ matrix.index }}
        env:
          CYPRESS_BASE_URL: http://localhost:3000

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: cypress-results-${{ matrix.index }}
          path: |
            cypress/videos/
            cypress/screenshots/
            cypress/reports/
          retention-days: 7

  collect:
    needs: cypress-run
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Download all results
        uses: actions/download-artifact@v4
        with:
          pattern: cypress-results-*
          merge-multiple: true
          path: all-results/

      - name: Upload combined results
        uses: actions/upload-artifact@v4
        with:
          name: cypress-combined-results
          path: all-results/

Persisting Timing Weights

The spec weights file is what makes distribution improve over time. Two strategies:

Strategy 1: Commit to git

git add cypress/parallel-weights.json
git commit -m "chore: update cypress parallel weights"

Works well if CI pushes back to the repo, or if weights are generated locally and committed manually.

Strategy 2: CI cache

- name: Restore weights
  uses: actions/cache@v3
  with:
    path: cypress/parallel-weights.json
    key: cypress-weights-${{ github.ref }}
    restore-keys: cypress-weights-

- name: Run tests
  run: npx cypress-parallel -s cy:run -t 4 -m ${{ matrix.index }}

- name: Save weights
  uses: actions/cache@v3
  with:
    path: cypress/parallel-weights.json
    key: cypress-weights-${{ github.ref }}

Cache strategy works without git access from CI but may miss weights on first run of a new branch.

GitLab CI Setup

cypress-tests:
  image: cypress/included:13.0.0
  parallel: 4
  script:
    - npm ci
    - |
      # GitLab sets CI_NODE_INDEX (1-based) and CI_NODE_TOTAL
      WORKER_INDEX=$(( CI_NODE_INDEX - 1 ))
      npx cypress-parallel -s cy:run -t $CI_NODE_TOTAL -m $WORKER_INDEX
  artifacts:
    when: always
    paths:
      - cypress/videos/
      - cypress/screenshots/
      - cypress/reports/
    expire_in: 1 week
    reports:
      junit: cypress/reports/junit-*.xml

Note: GitLab's CI_NODE_INDEX is 1-based, but cypress-parallel is 0-based. The $(( CI_NODE_INDEX - 1 )) subtraction handles this.

Generating JUnit Reports

Cypress's built-in reporter doesn't output JUnit. Add cypress-multi-reporters:

npm install -D cypress-multi-reporters mocha-junit-reporter

cypress.config.js:

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  reporter: 'cypress-multi-reporters',
  reporterOptions: {
    configFile: 'reporter-config.json',
  },
  e2e: {
    // ...
  },
});

reporter-config.json:

{
  "reporterEnabled": "spec, mocha-junit-reporter",
  "mochaJunitReporterReporterOptions": {
    "mochaFile": "cypress/reports/junit-[hash].xml"
  }
}

The [hash] in the filename creates unique files per spec — necessary when running in parallel to avoid overwriting.

Filtering Which Specs Run

cypress-parallel supports filtering specs:

# Only run specs matching a pattern
npx cypress-parallel -s cy:run -t 4 -m 0 --spec <span class="hljs-string">"cypress/e2e/checkout/**"

<span class="hljs-comment"># Exclude slow specs from parallel run
npx cypress-parallel -s cy:run -t 4 -m 0 --ignore <span class="hljs-string">"cypress/e2e/heavy/**"

Or set in package.json:

{
  "scripts": {
    "cy:parallel": "cypress-parallel -s cy:run -t 4 -m",
    "cy:serial": "cypress run --spec 'cypress/e2e/heavy/**'"
  }
}

Comparing with Cypress Cloud

Feature cypress-parallel Cypress Cloud
Cost Free Paid ($0-$400+/mo)
Timing-based distribution
Real-time dashboard
Automatic load balancing File-based Dynamic
Video storage CI artifacts Cloud
Flaky test detection
Setup complexity Low Very low

For teams already paying for CI minutes, cypress-parallel delivers the core parallelization benefit for free. The missing features (real-time dashboard, cloud video storage) matter less for teams with tight budgets.

Troubleshooting

All workers run the same specs: Worker index not being passed correctly. Verify ${{ matrix.index }} is 0-3, not 1-4.

Weights file not found: On first run, cypress-parallel distributes specs alphabetically. Run once to generate the weights file.

Some workers have no specs: Happens when spec count < worker count, or when spec patterns don't match. Check the resolved file list with --verbose flag.

Tests pass locally but fail in CI: Usually a timing issue. Add npx wait-on http://localhost:3000 before running Cypress if your dev server takes time to start.

Summary

cypress-parallel splits Cypress spec files across parallel CI workers using timing data from previous runs. Set up with npm install -D cypress-parallel, add matrix jobs in GitHub Actions (or parallel: N in GitLab), pass the worker index and total as -m and -t flags, and persist cypress/parallel-weights.json to improve distribution over time. No Cypress Cloud subscription required.

Read more