Skip to main content

Parallel runs

Taqwright is serial by default, one Appium, one device. To go parallel you either declare a local device pool and raise workers, or point at a cloud provider where each worker is its own independent session.

Device pools

Taqwright defaults to workers: 1, one Appium + one device, serial. To run tests truly in parallel against multiple devices, declare a device pool and bump workers:

taqwright.config.ts
import { defineConfig, Platform } from 'taqwright';

export default defineConfig({
workers: 3,
fullyParallel: true,
projects: [{
name: 'android-parallel',
use: {
platform: Platform.ANDROID,
device: {
provider: 'emulator',
pool: [
{ udid: 'emulator-5554', name: 'Pixel_7_API_34' },
{ udid: 'emulator-5556', name: 'Pixel_7_API_34_2' },
{ udid: 'emulator-5558', name: 'Pixel_7_API_34_3' },
],
},
buildPath: './app.apk',
appBundleId: 'com.example.app',
appium: { autoStart: true, port: 4723 },
},
}],
});

How worker partitioning works:

  • Worker N picks pool[N]. A worker where N >= pool.length fails fast with a clear error, never silently double-books a device.
  • Each worker spawns its own Appium on basePort + N when appium.autoStart: true, and kills it when the worker tears down.
  • Driver-specific ports (appium:systemPort, appium:wdaLocalPort, appium:chromedriverPort, appium:mjpegServerPort) are auto-staggered per worker so two UiAutomator2 / XCUITest sessions don't fight over the same port.

For iOS, the WDA / MJPEG port + DerivedData staggering is handled by iosParallelCaps(), one slot per separate iOS project; pool-driven parallelism within a single iOS project is auto-staggered per worker.

workers × fullyParallel × device.pool

fullyParallel only changes Playwright's scheduling granularity (tests-within-a-file vs whole-files per worker), it never raises the concurrent-device count, which is bounded by workers. So the only thing that risks device contention is workers > 1, and that's caught at config load.

workersfullyParalleldevice.poolResultWhat to do
1 (default)false (default)noneSafe. Serial, one Appium + one device.Nothing, this is the default.
1truenoneSafe. Still serial (one worker). fullyParallel only re-orders the schedule.Harmless; no real effect with one worker.
> 1anynoneRejected at config load. defineConfig throws before any device work.Add a device.pool with at least workers entries, or set workers: 1.
> 1anypool, length < workersRejected at config load. Under-sized pool.Grow the pool to at least workers entries, or lower workers.
> 1truepool, length >= workersCorrect parallel. Each worker gets pool[idx] + its own Appium + staggered ports.This is the intended parallel setup.
> 1falsepool, length >= workersParallel across files only. Workers run different files concurrently; tests within a file stay sequential.Set fullyParallel: true to also distribute tests within a file.

:::caution Don't run multiple workers against one device Without an adequately sized pool, workers > 1 is rejected with a clear error at config load, taqwright test aborts before any device work, rather than letting multiple workers collide on one Appium session. Either set workers: 1, or declare a device.pool with at least workers entries. :::

Cloud projects (browserstack / lambdatest) have no device.pool and the provider manages its own device queueing, so the config-load check skips them.

Parallel runs on BrowserStack / LambdaTest

Cloud providers need no device.pool. Each Playwright worker is its own OS process and opens its own independent cloud session, so parallelism is plain workers: N + fullyParallel: true, the provider queues device contention on its side.

taqwright.config.ts
import { defineConfig, Platform } from 'taqwright';

export default defineConfig({
workers: 5, // 5 parallel cloud sessions, no device.pool
fullyParallel: true, // defineConfig defaults to false (serial)
projects: [{
name: 'browserstack',
use: {
platform: Platform.ANDROID,
device: {
provider: 'browserstack', // or 'lambdatest'
name: 'Google Pixel 8',
osVersion: '14.0',
orientation: 'portrait', // optional
},
buildPath: 'bs://<app-id>', // pre-uploaded; see note below
appBundleId: 'com.example.app',
trace: 'on-failure',
video: 'off', // cloud records server-side
},
}],
});
export BROWSERSTACK_USERNAME=... # or LAMBDATEST_USERNAME
export BROWSERSTACK_ACCESS_KEY=... # or LAMBDATEST_ACCESS_KEY

npx taqwright test --project browserstack --workers 5
  • Parallelism is workers: N, nothing else. No device.pool, no per-worker Appium, no driver-port staggering, that machinery is local-only. One worker = one cloud session; pass/fail is pushed to the provider dashboard on teardown.
  • Credentials come from the environment: never in the config. Missing creds fail the worker fast with the provider's own message.
  • --workers N overrides the config value for that run, so you can dial parallelism per-run without editing the config.
  • Video is recorded server-side and viewed on the provider dashboard, taqwright attaches no mp4 for cloud runs. Trace still works and lands in the HTML report.
  • resetBetweenTests still type-requires buildPath + appBundleId, but the on-device terminate/reinstall is skipped, isolation comes from a fresh session per test plus the provider's own reset.

:::caution Two cloud gotchas (1) Bound workers to your provider plan's parallel-session limit, extra workers just block waiting for a free slot. (2) The build upload runs once per worker process and the cached URL doesn't cross worker boundaries, so a local .apk/.ipa in buildPath is uploaded N times for N workers. Pre-upload once and reference the returned bs:// / lt:// URL to skip the upload entirely. :::