Parallel Execution
Two built-in ways to run tests in parallel:
run-workers— split tests across worker threads on one runner.--shard— split test files across CI machines in a matrix build.
For anything more specific — your own grouping, a config per group, several browsers at once — drive the Workers API from a script (see Custom parallelization).
Workers
Section titled “Workers”run-workers <N> spawns N worker threads, each an independent CodeceptJS instance running a slice of the suite, and merges the results:
npx codeceptjs run-workers 4Steps are not streamed to the console in this mode — output from separate threads can’t be interleaved cleanly. While workers run, CodeceptJS sets process.env.RUNS_WITH_WORKERS=true, so plugins and helpers can branch on it. All run options work here too: --grep "@smoke", -c codecept.conf.js, --debug, and the rest. With --grep, only matching tests are spread across workers, so a narrow pattern may start fewer workers than requested.
By default, workers are created with a staggered delay of 200ms to prevent CPU spikes and stagger browser initializations. You can adjust this via workerInitializationDelay in your configuration.
Distribution strategies
Section titled “Distribution strategies”--by controls how tests spread across workers:
--by | How tests are assigned | Use when |
|---|---|---|
test (default) | each test pinned to a worker up front | tests take roughly equal time |
suite | each suite pinned to a worker; its tests stay together | suites share a BeforeSuite you don’t want repeated |
pool | workers pull the next test from a shared queue as they free up | test durations vary — best load balancing |
npx codeceptjs run-workers 4 --by pool--suites is shorthand for --by suite.
Serial tests
Section titled “Serial tests”Some tests can’t share the stage: they change global settings, reset data, or toggle a feature flag that other tests read. Mark them with a tag and set the serial pattern in config or pass it with --serial:
export const config = { serial: '@serial',}Scenario('change account timezone @serial', ({ I }) => { // ...})run-workers keeps matching tests out of the parallel workers. When those workers finish, one more worker starts and runs the serial tests one by one. It is still a single run: bootstrapAll, teardownAll and event.workers.result fire once, and the exit code covers both parts.
Test distribution: Worker 1: 14 tests Worker 2: 13 tests Worker 3: 4 tests (serial)Pass --serial to set or override the pattern for a single run:
npx codeceptjs run-workers 3 --serial "@serial"serial is matched against the full test title the same way --grep is, so a tag on a Feature makes all its scenarios serial. It works with every --by strategy and combines with --grep: only serial tests that also match grep are run. With multiple browsers each profile gets its own serial worker, started one after another.
Multiple browsers
Section titled “Multiple browsers”Define browser profiles under multiple in codecept.conf.js:
multiple: { default: { browsers: ['chrome', 'firefox'] },}Then run a profile across workers — by name, or all for every profile:
npx codeceptjs run-workers 3 defaultnpx codeceptjs run-workers 3 all(run-multiple runs the same profiles in separate subprocesses instead of threads — see npx codeceptjs run-multiple --help.)
Reading worker results
Section titled “Reading worker results”When all workers finish, the run fires event.workers.result with the merged result:
import { event } from 'codeceptjs'
export default function () { event.dispatcher.on(event.workers.result, result => { console.log(result.hasFailed() ? 'FAILED' : 'PASSED', result.stats) for (const test of result.tests) { console.log(test.title, test.duration, 'ms', `worker ${test.workerIndex}`) } })}For end-of-run work like publishing to a test-management tool, listen on event.workers.result (fires once) rather than event.all.result (fires in every worker).
Sharding
Section titled “Sharding”--shard <index>/<total> runs only a slice of your test files: the file list is cut into total even chunks and this run executes chunk index. It is built for CI matrices — one machine per shard, each running run:
npx codeceptjs run --shard 1/4npx codeceptjs run --shard 2/4GitHub Actions:
jobs: test: runs-on: ubuntu-latest strategy: matrix: shard: ['1/4', '2/4', '3/4', '4/4'] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npx codeceptjs run --shard ${{ matrix.shard }}Add or remove tests freely — shards rebalance automatically.
Custom parallelization
Section titled “Custom parallelization”When the built-in commands aren’t enough, build a runner with the Workers API: decide which tests go to which group, give each group its own config, and listen for results.
#!/usr/bin/env nodeimport { Workers, event } from 'codeceptjs'
const workers = new Workers(null, { testConfig: './codecept.conf.js' })
// split the suite into 2 groups, run each group on two browsersconst groups = await workers.createGroupsOfSuites(2)for (const browser of ['chromium', 'firefox']) { for (const group of groups) { const worker = workers.spawn() worker.addTests(group) worker.addConfig({ helpers: { Playwright: { browser } } }) }}
workers.on(event.test.failed, t => console.log('FAIL', t.title))workers.on(event.all.result, () => workers.printResults())
await workers.bootstrapAll()try { await workers.run()} finally { await workers.teardownAll()}Building blocks:
new Workers(N, { testConfig, options })—Nworkers; passnullto spawn them yourself withspawn().await createGroupsOfTests(n)/await createGroupsOfSuites(n)— split the suite intongroups (async: test files are loaded as ES Modules).worker.addTests(group)/worker.addConfig(partialConfig)— assign tests and config overrides to a spawned worker.bootstrapAll()→run()→teardownAll()— lifecycle (wraprun()intry/finallyso teardown always runs).- Events on the
workersobject:event.test.passed,event.test.failed,event.all.result, plus'message'for anything a child worker sends.printResults()prints the standard summary;result.hasFailed()andresult.statsgive the totals.
To split by your own rule, pass a function as by — it receives the worker count and returns an array of file groups:
const splitTests = () => [ ['./test/login_test.js', './test/signup_test.js'], // group 1 ['./test/slow_checkout_test.js'], // group 2]
const workers = new Workers(2, { testConfig: './codecept.conf.js', by: splitTests })workers.on(event.all.result, () => workers.printResults())await workers.run()Sharing data between workers
Section titled “Sharing data between workers”Worker threads don’t share memory. share() publishes a value that any worker reads with inject():
// in any test or hookshare({ user: { name: 'jane', password: 's3cret' } })
// anywhere else, even in another workerconst { user } = inject()Seed shared state before tests run from bootstrap():
export const config = { bootstrap() { share({ user: null }) },}Shared data is a Proxy. Don’t reassign the injected object itself (let d = inject(); d = {…} breaks the link); mutate it or call share() again. Pass { local: true } to keep a value inside one worker:
share({ tmpFile: '/tmp/run-1' }, { local: true })Scaling beyond one machine
Section titled “Scaling beyond one machine”Worker threads split the suite, but every Playwright worker still runs a full local browser.
With the Kitesurf helper each worker drives a cloud browser on Cloudflare
instead — run-workers 16 means sixteen browsers spawned in about a second, none of them
competing for your runner’s CPU. See Alternative Browser Engines for
the full setup.