Back to News & Insights
JavaScript October 6, 2026 · 7 min read

Bun 1.4's bun test --parallel cuts a 4-second suite to about 1 second

I measured Bun 1.4's new bun test --parallel flag: a 4.04s suite dropped to 1.03s on 4 workers for IO-bound tests, but a CPU-bound suite plateaued at the same 4 workers and going higher made it worse.

Bun 1.4's bun test --parallel cuts a 4-second suite to about 1 second

I had a twenty-file test suite that took just over four seconds to run. I added one flag, --parallel, and it took one second. Then I tried to push it further and found the exact point where the flag stops helping, which turned out to depend on what kind of work the tests were actually doing, not on how many files there were.

Bun 1.4 shipped on 20 August 2026 and brought bun test --parallel, which distributes test files across worker processes instead of running them one after another in a single process. I'm running Bun 1.4.2 on a 4-core Intel Xeon VM (KVM, 4 logical CPUs, no cgroup limit), and I wanted to know what the flag actually buys you, where it tops out, and what the "implies --isolate" line in the docs means in practice.

I wrote twenty test files, two tests each, where every test awaits a 100ms setTimeout before asserting — a stand-in for the API calls and debounces that make a lot of real integration tests slow. Three runs each, fastest and slowest within 15ms of each other:

| Mode | Wall time | |---|---| | bun test (serial) | 4.04s | | --parallel=1 | 4.06s | | --parallel=2 | 2.05s | | --parallel=4 (default, = CPU count) | 1.03s | | --parallel=8 | 0.63s |

With no argument, --parallel defaults to the number of CPU cores, which on this box is 4. That alone took the suite from 4.04s to 1.03s, a 3.9x speedup that lines up almost exactly with four workers splitting twenty files. What surprised me is that --parallel=8 kept improving, down to 0.63s, on a machine with only four logical cores. I'll get to why in a minute, because it's the most important part of this post.

A test that awaits setTimeout isn't actually using the CPU while it waits. It's just occupying a slot. So I built a second suite, same shape, twenty files and two tests each, but this time each test runs a real computation: a prime sieve up to 20 million, which takes genuine CPU time with nothing to wait on.

| Mode | Wall time | |---|---| | bun test (serial) | 6.1s | | --parallel=1 | 6.2s | | --parallel=2 | 3.3s | | --parallel=4 | 1.8s | | --parallel=6 | 1.9s | | --parallel=8 | 1.8s |

This is the number that argues against the headline. Scaling is close to linear up to four workers, matching the four physical cores, and then it stops. Going to six or eight workers bought nothing, and in a couple of runs was a touch slower than four, presumably from context-switch overhead as processes fight for the same cores. --parallel schedules OS processes; it doesn't make more CPU appear. For a suite that's actually CPU-bound, the number that matters isn't how many files you have, it's nproc.

Put the two tables together and the real rule is simpler than either one alone: --parallel can scale past your core count for tests that spend most of their time waiting, and it cannot for tests that spend most of their time computing. Most real suites are a mix of both, so the honest advice is to benchmark your own suite rather than assume either curve.

The docs say --parallel implies --isolate, and describe it as giving "each file... a fresh global object even when two files land on the same worker." I wanted to know exactly what that resets, because the phrasing leaves open whether it's per-file or something coarser or finer.

I wrote a module with a plain top-level counter and four test files that import it and log what they see:

Under plain bun test, the whole run shares one process and one module cache, so the counter climbs across files in whatever order they happen to run:

All four ran in the same worker process (same PID), so this isn't about separate OS processes — it's a fresh JS global per file inside one process, exactly as documented. Adding --no-isolate to --parallel brought the leak straight back: 1, 2, 3, 4 again, same as serial.

The part the one-line doc summary doesn't spell out: isolation is per file, not per test. Three tests inside a single file, run under --parallel, saw the counter climb 1, 2, 3 with no reset between them. If your tests rely on module-level setup running fresh for every single test, not just every file, --isolate won't give you that, parallel or not.

Bun also sets BUNTESTWORKERID and JESTWORKER_ID to the worker's 1-based index, which I confirmed by printing both in the test above — worth knowing if you're porting a Jest setup that keys a database name or port off that variable.

The docs describe it precisely: "the coordinator handles --bail at file granularity: once the failure threshold is reached it starts no new files, but files already running finish." I tested this against twenty files where the first one fails a 300ms-long test.

Without --bail, under --parallel, all twenty files ran regardless of the failure:

Four files ran, not one. That matches four workers already mid-flight when the failure landed; the other sixteen files never started. This is a real time saving in CI (1.53s down to 0.32s here), but it also means --bail under --parallel won't tell you about any other failures in those sixteen untouched files, including ones with nothing to do with whatever broke the first.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation