skip to content

In Playwright, why can a few test.describe.serial groups dominate a large suite's wall-clock?

level: seniorimportance: nice to knowfreq 32%

answer

  1. Parallelism works by splitting work
  2. Serial forbids the split
  3. Chain duration becomes a floor
  4. Retries replay the whole chain
  5. Shrink the span, not the machine

basics

~20 s

A serial group is indivisible: its tests run in order on one worker, so its total duration is a floor no extra parallelism can cut. Retries replay the whole group, so the longest chain sets the finish time.

solid answer

~40 s

Parallelism can only shorten a run by splitting work, and `test.describe.serial` explicitly forbids splitting the block it guards. However many workers are available, every test in the group runs one after another in the same worker, so the group's summed duration becomes a lower bound on the run. Other files finish and their workers go idle while the long chain is still walking its steps, which is why a suite can add capacity and see almost no improvement. Retries make it worse in a specific way: a failure inside a serial group replays the group from its first test, so the chain's whole duration is paid again per attempt. The fix is structural — shorten the chain so only the genuinely dependent tests stay serial, and let the rest go parallel.

code

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

// Independent checks stay parallel and finish with the pack.
test('tax table renders', async ({ page }) => {
  await page.goto('/payroll/tax-tables');
  await expect(page.getByRole('heading', { name: 'Tax tables' })).toBeVisible();
});

// Only the genuine two-step handoff stays serial.
test.describe.serial('period close handoff', () => {
  test('locks the period', async ({ page }) => {
    await page.goto('/payroll/periods/current');
    await page.getByRole('button', { name: 'Lock period' }).click();
    await expect(page.getByText('Locked')).toBeVisible();
  });

  test('hands off to the ledger', async ({ page }) => {
    await page.goto('/payroll/ledger');
    await expect(page.getByText('Handoff complete')).toBeVisible();
  });
});

go deeper

for a junior

Remember that a serial group runs on one worker, so extra workers cannot make that group finish any sooner.

for a middle

Explain the arithmetic: the chain's summed duration is a floor on the run, and a retry pays that whole duration again.

for a senior

Diagnose it from run shape rather than guesswork, spotting the idle tail, then shrink the serial span to the tests that genuinely depend on each other.

for a principal

Weigh feedback time against the comfort of long dependent chains, and set the expectation that capacity cannot buy back what the suite has declared indivisible.

## Why a serial group is a floor Parallel execution shortens a run by splitting work across workers. `test.describe.serial` is a declaration that a particular block **must not be split**: its tests run in declaration order inside one worker process. That makes the group's summed duration a hard floor: - A four-test chain of ninety seconds each takes six minutes, and six minutes is what it takes whether the machine offers four workers or forty. - Once every other file is done, the remaining workers sit idle while the chain finishes, so measured concurrency drops off a cliff near the end of the run. - The run's finish time is therefore `max(longest serial chain, everything else spread over the workers)`, and past a certain suite size the first term wins. This is the shape people describe as "we doubled the machine and the CI time barely moved". ## Retries multiply the floor Serial groups do not resume mid-chain. A failure replays the block from its first test, so: 1. Attempt one costs the whole chain. 2. Each retry costs the whole chain again, including the tests that already passed. 3. A six-minute chain with two retries can occupy a worker for eighteen minutes on its own. A short flaky step at the end of a long chain is the worst case, because the cheap failure drags an expensive prefix along with it every time. ## Where the time actually goes | symptom | likely cause | what it looks like in the report | |---|---|---| | Adding workers changes nothing | one long serial chain | most tests finish early, a single file runs on | | Total time spikes on flaky days | serial group replayed on retry | repeated attempts on tests that passed | | Long idle tail at the end of a run | indivisible block scheduled late | few tests in flight, low overall concurrency | | A file's own tests never overlap | file-level default or serial mode | consistent ordering, single worker for the file | ## Shortening the chain The cure is to make the serial span as small as the actual dependency, not as large as the feature: - Split a long `test.describe.serial` block into a small serial core plus independent tests around it, and let the independent ones run in parallel. - Check every test in the chain for whether it truly needs its predecessor, or whether it was simply written next to it in a payroll close scenario. - Prefer `test.describe.configure({ mode: 'default' })` when the tests only need to avoid overlapping, since a failure then costs one test rather than a replay of the block. - Push setup that every step repeats out of the chain, so the replay cost per attempt drops even when the chain stays. ## Scheduling reality to keep in mind - A serial group does not block the rest of the suite; other files run beside it. It bounds the run only because it cannot itself be accelerated. - Ordering guarantees are per scope. Two serial groups in different files still run concurrently with each other. - The cost is wall-clock, not correctness. A suite full of serial chains can be perfectly green and still be the reason CI feedback takes half an hour. ## How to answer Say the mechanism in one line — "serial forbids splitting, so the chain's duration is a floor no worker count can cut" — then add the retry multiplier, then land on the structural fix: shrink the serial span to the genuinely dependent tests instead of buying more capacity that the chain cannot use.

  • How would you spot that a serial group, not capacity, is setting your run time?
    Look at where the run's time goes rather than its total. If most tests finish early and one file keeps running with the other workers idle, the tail is an indivisible block. Adding capacity would leave that tail exactly as long.
  • Does splitting a serial group into smaller serial groups help?
    Yes, if the split is honest. Two shorter independent chains can run on two workers at once, so the floor becomes the longer of the two rather than their sum, and a retry replays only the chain that failed instead of everything.

Ten checkouts do not speed up one customer with a hundred items; the queue that cannot be divided sets closing time.

saying these in an interview costs you the question

  • Says more workers always shorten the run
  • Thinks a serial group blocks other files too
  • Ignores that retries replay the entire chain
  • Treats a green run as evidence of good scheduling
  • Adds machines instead of shortening the chain