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:
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
Npickspool[N]. A worker whereN >= pool.lengthfails fast with a clear error, never silently double-books a device. - Each worker spawns its own Appium on
basePort + Nwhenappium.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.
workers | fullyParallel | device.pool | Result | What to do |
|---|---|---|---|---|
1 (default) | false (default) | none | Safe. Serial, one Appium + one device. | Nothing, this is the default. |
1 | true | none | Safe. Still serial (one worker). fullyParallel only re-orders the schedule. | Harmless; no real effect with one worker. |
> 1 | any | none | Rejected at config load. defineConfig throws before any device work. | Add a device.pool with at least workers entries, or set workers: 1. |
> 1 | any | pool, length < workers | Rejected at config load. Under-sized pool. | Grow the pool to at least workers entries, or lower workers. |
> 1 | true | pool, length >= workers | Correct parallel. Each worker gets pool[idx] + its own Appium + staggered ports. | This is the intended parallel setup. |
> 1 | false | pool, length >= workers | Parallel 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.
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. Nodevice.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 Noverrides 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.
resetBetweenTestsstill type-requiresbuildPath+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.
:::