This is not a "Bun is slow" post. In my case, Bun was often fast. I switched because I stopped trusting the failure modes in the part of my test suite that spawned real processes and local infrastructure.
I was testing a Cloudflare Worker app with a mix of:
- plain unit tests
- tests that spawn
wrangler dev - local mock APIs
- MSW interception
- socket/network activity
- OAuth/MCP integration flows
Originally this all ran under bun test.
A few of the root causes were my own test-harness bugs, not Bun bugs:
- MSW was intercepting loopback traffic for local mock servers without matching passthrough handlers.
- I had multiple global MSW servers stacking on top of each other in one area of the suite.
- Some tests were just testing mock infrastructure, and I deleted them instead of preserving them through the migration.
So I do not think it is fair to say Bun caused every failure I saw.
That said, the practical Bun Test pain was still real:
The most frustrating failures were not normal red/green failures. They were hangs. In CI I would see things like:
- a test sitting until timeout
killed 1 dangling process- 60s timeouts before I got useful feedback
That is the class of failure that made me lose confidence in the runner for this suite.
When a test spawns subprocesses, dev servers, or opens sockets, the important question is:
what resource is still alive, and why?
What I wanted was a much more actionable answer than "there was a dangling process". I needed to know whether the issue was:
- my code
- the spawned
wrangler devprocess - a socket/server handle
- the test runner waiting on cleanup
- some interaction between those pieces
The lack of precise diagnostics made debugging much slower than it needed to be.
A big part of the pain was simply waiting around for hangs to surface. Local timeouts were too long for a suite with this kind of failure mode. Even when I fixed some real harness bugs, the developer experience still felt bad because every bad cleanup path cost a lot of time.
Parts of my suite relied on Bun-specific APIs like:
Bun.spawnBun.servemock.moduleBun.inspect
Those APIs are useful, but they also meant the test harness was coupled to Bun's runtime model. Once I decided I wanted a split-runtime setup, I wasn't just changing test runners, I was rewriting the harness.
What I ended up with was a clearer split:
node-unitfor ordinary Node-friendly testsworkers-unitfor Cloudflare-specific tests using the Cloudflare Vitest integrationmcp-e2efor the expensive integration suite
That gave me a few things I was missing before:
- an explicit runtime boundary
- clearer isolation between test categories
- a better fit for Worker-specific dependencies
- more confidence that a hung test was the test, not the overall runner setup
I also sped up the slowest MCP integration suite by sharing immutable setup safely: create one seeded database baseline once, then clone it per test. That preserved isolation while cutting that suite from roughly 42s to roughly 12s.
If the Bun team wants practical feedback from someone who switched away, these would be the top items for me:
- Better diagnostics for dangling child processes, sockets, and watchers.
- Clearer distinction between "the test timed out" and "the runner is waiting on leaked resources".
- More actionable leak reporting at the end of a run.
- A stronger story for process-heavy integration tests.
- Easier runtime partitioning when part of the suite wants one environment and another part wants something else.
I did not migrate because Bun was obviously slower on raw execution.
I migrated because I needed:
- more predictable isolation
- better failure signals
- cleaner runtime boundaries
- less time spent debugging hangs
If Bun Test improves the diagnostics and ergonomics around leaked resources and process-heavy integration tests, I would be much more likely to try it again.