bun test --parallel hangs after all tests pass (teardown deadlock) — not reproducible without --parallel, no single culprit file
bugneeds triage
### What version of Bun is running?
`1.3.14`
### What platform is your computer?
**CI:** GitHub Actions `ubuntu-latest` (Linux x86_64)
### What steps can reproduce the bug?
We run a ~4,400-test / ~450-file Next.js + React Testing Library suite via:
```
bun test --parallel --timeout=20000 <files...>
```
split into 4 shards with `--shard=N/4`. One shard (call it shard A, a fixed set of ~112 files under a deterministic `--shard` split) consistently hangs after all tests finish: every test reports `(pass)`, but the process never prints the final `Ran N tests across M files` summary and never exits. We wrap the run in a watchdog that kills the process after 120s of no new stdout — by that point every individual test has already logged pass with 0 failures, so the hang is strictly a **post-test-completion** hang, not a stuck test.
### Isolating the cause: same file set, only the run mode changes
We bisected shard A's 112 files down to a 28-file slice (`q1`) that reliably reproduces the hang, then ran that exact same 28-file list three different ways:
| Command | Result | Time |
|---|---|---|
| `bun test --parallel --timeout=20000 <q1 files>` | **Hangs** — no final summary, watchdog has to kill it | — |
| `bun test --timeout=20000 <q1 files>` (single process, no `--parallel`) | Completes normally — `Ran 224 tests across 28 files`, 0 fail | 52s |
| `bun test --isolate --timeout=20000 <q1 files>` (single process, per-file isolation) | Completes normally — 224 pass / 0 fail | 337s |
Since the identical test files complete cleanly both without `--parallel` and with `--isolate` (single process), the hang is not caused by a leaked handle/timer/connection in our test code or its dependencies — it only appears when `--parallel` spawns/tears down worker subprocesses.
### No single culprit file — it's an emergent, count-dependent race
We tried to narrow `q1` (28 files) down further to find "the" offending file:
| Slice tested with `--parallel` | Result |
|---|---|
| `q1` — 28 files | **Hangs** |
| `e1` — first 14 files of `q1` | Clean |
| Suspected file (from earlier narrowing) run alone + 1 unrelated file (2 files total) | Clean |
So the hang doesn't reproduce with a small number of files, but does once enough files accumulate (somewhere between 14 and 28 in this slice) — it looks like teardown of N parallel workers races once the worker pool has enough concurrent/queued work, rather than one file leaking a specific resource. This matches what we see across all 4 shards in CI: they're roughly the same file count (~110 each), but only one shard's specific *composition* of files reliably crosses whatever internal threshold triggers the race — the other 3 shards finish and exit normally in the same CI run.
### What we ruled out before landing on this
- **Not `--only`/`.only`-related** — no `.only` in the suite.
- **Not a leaked `setTimeout`/`setInterval` from app code** — we already wrap `setTimeout`/`setInterval` globally in a test preload to call `.unref()`, and explicitly `jsdomWindow.close()` in `afterAll`, before this investigation. Those fixes eliminated most hangs; this is what's left.
- **Not `mock.module` registry leakage across files** (a separate, known Bun behavior we work around already) — the hang reproduces with a plain single-process run of the exact same files, so cross-file mock state isn't at play here; the failure mode is specifically tied to the parallel worker pool's teardown.
- **Not resolved by reducing to sequential CI shards of the same size** — a 5-way shard split (vs our normal 4-way) still hung on one shard, so it's not a fixed "shard index" issue, just whichever shard's file set crosses the threshold.
### What is the expected behavior?
`bun test --parallel` should exit with a summary once every test file has reported pass/fail, the same way a non-`--parallel` run does — regardless of how many files/workers are involved.
### What do you see instead?
_No response_
### Additional information
- We work around this in CI with a watchdog wrapper that kills the process after N seconds of stdout silence and judges pass/fail from the already-emitted per-test output (0 fails + no "Unhandled error between tests" + at least one file ran ⇒ treat as success). This works, but it means the final coverage report (`bun test --coverage`, which writes `lcov.info` at the very end) is lost for whichever shard hits the hang that run, since the process is killed before it flushes coverage output.
- `--isolate` avoids the hang entirely (runs each file in its own context within one process) but is ~6x slower for us (~5.5 min → ~5.5s per 28 files vs the numbers above scaled up would put a ~112-file shard at ~20+ min), which is not viable for our CI budget.
- Happy to share the full file list / a minimal reproduction repo if useful — our current repro is entangled with an internal test preload (jsdom setup, MSW, module-mock guard), so we'd need to strip it down to a synthetic suite first. Let us know if that would help and we'll prepare one.
关闭于 2026-07-15 1 条评论