Skip to content

maxConcurrency is not enforced across sibling describe blocks: concurrent tests pile up between beforeEach and afterEach #10530

@dbousamra

Description

@dbousamra

Describe the bug

In vitest 4, maxConcurrency does not bound the number of concurrent tests that are mid-lifecycle (between beforeEach and afterEach) when the tests are split across sibling describe blocks. Setups run far ahead of teardowns, so resources created per-test in beforeEach (in our real suite: one Postgres database + app instance per test) accumulate far beyond maxConcurrency.

This is a follow-up to #10097, which was fixed by #10179 (v5) and backported in #10384 (v4.1.7) (thank you @hi-ogawa!). That fix restores the bound within a single suite: a flat describe.concurrent now behaves correctly (it did not in 4.1.6). However, the fix gives each task branch an independent limiter, so sibling concurrent suites each receive their own full maxConcurrency budget and the totals multiply. With 5 sibling groups and maxConcurrency: 5, up to 25 tests are mid-lifecycle simultaneously.

I've created a repro. Basically we have 20 tests in 5 sibling describe groups under describe.concurrent, with maxConcurrency: 5. Each test creates a fake db in beforeEach and drops it in afterEach. Every db operation costs more the more databases are live (mimicking how a real database degrades under load). An afterAll asserts the live-database high-water mark stayed at or below 6. This is a contrived example, so if anyone has a better approach lmk. The point though is that there's definitely a regression in V4 causing real world consequences.

Results:

Version Flat (1 suite) Grouped (5 sibling suites)
3.2.4 max 5 live max 5 live
4.1.6 20 live 20 live
4.1.7 (with #10384) max 5 live 20 live
5.0.0-beta.2 (with #10179) max 5 live 20 live

On the failing versions the suite fails with:

AssertionError: live databases should be bounded by maxConcurrency: expected 20 to be less than or equal to 6 and wall time roughly doubles (~3.1s to ~6.5s) because the fake database's load-scaled operations make the pile-up cost real time. Flattening the groups (all 20 tests directly under one describe.concurrent) makes 4.1.7 and 5.0.0-beta.2 pass, so the sibling suites are required to reproduce.

Why this happens

These per-branch limiters do not compose into a global bound. With N concurrent sibling suites, the effective limit on mid-lifecycle tests is roughly maxConcurrency * N, not maxConcurrency, which is why the flat repro passes on 4.1.7 while the grouped one allocates all 20 dbs at once.

For certain test suites this is a large performance regression. For example, our server suite went from 38s on vitest 3 to 89s on vitest 4 with identical configuration and identical per-test timings, purely from the resource contention caused by the pile-up. A fix likely needs the per-branch limiters to share a global budget, or a test to hold one slot for its full setup and teardown lifecycle as in v3.

Reproduction

https://stackblitz.com/edit/vitest-dev-vitest-ruovhudo?file=package.json

Change v3 to v3 and vice versa in the package.json to see failures

System Info

System:
    OS: macOS 26.3
    CPU: (18) arm64 Apple M5 Pro
    Memory: 176.59 MB / 48.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.0.2 - /Users/dbousamra/.nvm/versions/node/v24.0.2/bin/node
    Yarn: 3.5.1 - /Users/dbousamra/.nvm/versions/node/v24.0.2/bin/yarn
    npm: 11.3.0 - /Users/dbousamra/.nvm/versions/node/v24.0.2/bin/npm
    bun: 1.3.3 - /Users/dbousamra/.bun/bin/bun
    Deno: 2.5.6 - /Users/dbousamra/.deno/bin/deno
  Browsers:
    Chrome: 148.0.7778.217
    Firefox: 148.0
    Safari: 26.3

Used Package Manager

pnpm

Validations

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions