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]. WorkerNwhereN >= 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 — what happens
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 now caught at config load.
workers | fullyParallel | device.pool | Result | What to do |
|---|---|---|---|---|
1 (default) | false (default) | none | Safe. Serial — one Appium + one device, one test at a time. | Nothing — this is the default. |
1 | true | none | Safe. Still serial (one worker). fullyParallel only re-orders the schedule; nothing runs concurrently. |
Harmless; it has no real effect with one worker. Leave it or drop it. |
> 1 | any | none | Rejected at config load. defineConfig throws; taqwright test aborts before any Appium/device work (no silent collision). |
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 — some workers would have no device. | 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 driver ports; every test fans out across the pool. |
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. | Fine for file-level parallelism. Set fullyParallel: true to also distribute tests within a file. |
Cloud exemption: browserstack / lambdatest projects have no device.pool (the field doesn't exist on their type) and the provider manages its own device queueing, so the config-load check skips them — workers > 1 is not rejected for a cloud-only project.
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.
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. The config-load pool check skips cloud-only projects, so workers > 1 is never rejected for them.
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://<hashed-app-id>', // pre-uploaded; see note below
appBundleId: 'com.example.app', // required for LambdaTest
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 so sessions don't sit "Running" until idle-timeout. - Credentials come from the environment (
BROWSERSTACK_USERNAME/BROWSERSTACK_ACCESS_KEY, orLAMBDATEST_USERNAME/LAMBDATEST_ACCESS_KEY) — never in the config. Missing creds fail the worker fast with the provider's own message, before any session opens. --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 dance is skipped — the build lives in the cloud. Isolation comes from a fresh session per test plus the provider's own reset.
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 (as above) — that path skips the upload entirely.