skip to content

In Playwright, what does test.info().workerIndex tell you inside a running test?

level: juniorimportance: should knowfreq 45%

answer

  1. Identifies the process, not the test
  2. Counting starts at zero
  3. Restarted workers never reuse a number
  4. Sibling field is bounded by worker count
  5. Worker fixtures read it from workerInfo

basics

~10 s

Playwright's test.info().workerIndex returns the number identifying the worker process running the test, starting at zero. Every worker process the run starts gets its own value, and a replacement worker receives a new, higher number.

solid answer

~40 s

`test.info()` hands you the current test's `TestInfo`, and `workerIndex` on it is the number of the worker process executing that test, counting from `0`. Each process Playwright starts gets a unique value, so a replacement worker after a failure gets a fresh, higher number rather than reusing the old one. That makes it ideal for tagging per-worker artefacts and log lines, and poor for indexing a fixed pool, since it can grow past the configured worker count. The companion field `parallelIndex` is the one bounded by the worker count and reused by a replacement, so pool slots should come from it. In a worker-scoped fixture you read the same fields from the third callback argument, `workerInfo`, instead of calling `test.info()`.

code

typescript · 9 lines
typescript
import { test, expect } from '@playwright/test';

test('audit log export is scoped to one worker', async ({ page }, testInfo) => {
  console.log(`worker ${test.info().workerIndex}, slot ${test.info().parallelIndex}`);
  await page.goto('/admin/audit');
  await page.getByRole('button', { name: 'Export' }).click();
  await expect(page.getByText('Export ready')).toBeVisible();
  await testInfo.attach('slot', { body: String(testInfo.parallelIndex) });
});

go deeper

for a junior

Know that it names the worker process running your test and starts at zero, and that it is a normal number you can print or use in a filename.

for a middle

Be able to say why a replacement worker gets a new number, and why that makes the bounded sibling field the right choice for reserving a slot in a fixed pool.

for a senior

Show where the read belongs: allocate the per-worker resource once in the worker-scoped fixture from the worker info argument, rather than recomputing it in individual tests.

for a principal

Frame it as provisioning. The index is the contract between the suite and the finite resources an environment gives it, so decide how many slots exist and who owns their lifecycle.

`test.info()` returns the `TestInfo` object for the test that is currently running, and `workerIndex` is the field on it that identifies the process doing the running. It is a plain number, starting at `0`, and it is the handle a suite uses when a worker needs something of its own. ## What the number means Playwright runs tests in separate worker processes. Every worker process that the run starts is given a **unique** `workerIndex`: the first is `0`, the next `1`, and so on. If a worker is stopped -- for example after a failure -- and Playwright starts a replacement to carry on, that replacement is a new process and gets a new, higher `workerIndex`. Nothing ever reuses an index. That property is exactly what makes `workerIndex` good for some jobs and bad for others. - **Good for:** labelling output. A log file, a dump directory or a debug line tagged with `workerIndex` is unambiguous, because two processes never share the number. - **Bad for:** indexing into a fixed pool. Over a long run with restarts, `workerIndex` can exceed the number of workers configured, so `pool[workerIndex]` eventually reads past the end. ## The sibling field: parallelIndex `TestInfo` also carries `parallelIndex`. It is the slot the worker occupies rather than the identity of the process: a number between `0` and one less than the worker count, and workers running at the same time always hold different values. When a worker is replaced, the replacement takes over the same `parallelIndex`. | | `workerIndex` | `parallelIndex` | |---|---|---| | Range | grows for the life of the run | `0` to workers minus one | | Uniqueness | unique across every process started | unique among workers running at once | | After a restart | a fresh, higher number | the same number, reused | | Use it for | naming artefacts and log lines | reserving one slot of a fixed pool | The rule of thumb is short: **identity is `workerIndex`, slot is `parallelIndex`.** A suite for an internal admin console that gives each worker its own stub webhook receiver on a port should compute the port from `parallelIndex`, so the ports stay inside a known range however many restarts the run survives. ## Where to read it from There are two places, and picking the wrong one is the usual mistake: 1. **Inside a test, a hook or a test-scoped fixture**, call `test.info()` and read `test.info().workerIndex`. `TestInfo` also exposes the worker-level fields, so both indices are reachable there. 2. **Inside a worker-scoped fixture**, use the **third callback argument**, `workerInfo`. A worker fixture is written as `async ({}, use, workerInfo) => { ... }`, and `workerInfo` carries `workerIndex`, `parallelIndex`, `project` and `config` -- the values that are constant for the whole worker, which is exactly the lifetime that fixture has. ## Why a fixture cares at all The reason these indices show up in fixture code is that worker scope creates one instance per worker, and a per-worker instance frequently needs a per-worker resource to go with it: a port to bind, a slot in a bounded pool, a directory to write into. Reading the index inside the fixture keeps that allocation in one place instead of scattering it through the tests. - Compute the resource from the index once, in the worker fixture's setup. - Prefer `parallelIndex` whenever the resource comes from a fixed, pre-provisioned set. - Use `workerIndex` when you want the value to be distinct for every process in the run's history, such as a filename you do not want a restarted worker to overwrite.

  • When would you use parallelIndex instead of workerIndex?
    Whenever the value indexes a fixed, pre-provisioned set: a port range, a bounded pool of connections, a set of reserved slots. `parallelIndex` stays between `0` and the worker count minus one and is reused by a replacement worker, so the set never needs more members than there are concurrent workers. `workerIndex` keeps climbing as workers are replaced and would eventually run off the end of that set.
  • How do you read the worker index inside a worker-scoped fixture?
    From the third argument of the fixture function. A worker fixture is written `async ({}, use, workerInfo) => { ... }`, and `workerInfo` exposes `workerIndex`, `parallelIndex`, `project` and `config`. Those are the values that stay constant for the fixture's whole lifetime, which is why the worker-scoped signature hands you `workerInfo` where a test-scoped fixture receives `testInfo`.

saying these in an interview costs you the question

  • Says workerIndex identifies the test rather than the process
  • Thinks it always stays below the configured worker count
  • Uses it to index a fixed pool that a restart then overruns
  • Believes a replacement worker keeps the old index
  • Confuses it with the retry number of the current test