taqwright

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:

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.

workersfullyParalleldevice.poolResultWhat to do
1 (default)false (default)none Safe. Serial — one Appium + one device, one test at a time. Nothing — this is the default.
1truenone 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.
> 1anynone 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.
> 1anypool, 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.
> 1truepool, 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.
> 1falsepool, 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.

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.

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.

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://<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
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 (as above) — that path skips the upload entirely.