new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Bun · Test runner · all subjects

concurrency & serial

33 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Force all tests concurrent with --concurrent flag

The bun test --concurrent command flag overrides bunfig.toml and forces all tests to run concurrently, regardless of glob patterns or file names.

test.serial() marks a test to run sequentially

In a file matched by concurrentTestGlob, you can use test.serial() to explicitly mark an individual test to run sequentially instead of concurrently. This provides fine-grained control over which tests in a concurrent file should still run one at a time.

test.concurrent() explicitly marks a test as concurrent

You can use test.concurrent() to explicitly mark a test to run concurrently. In files matched by concurrentTestGlob, plain test() is already equivalent to test.concurrent().

Tests run sequentially by default in non-matching files

Tests in files that do not match the concurrentTestGlob pattern run sequentially by default. This allows tests that share state or depend on ordering to stay sequential.

Migration strategy for concurrent tests

To migrate existing tests to concurrent execution: 1) Start with independent integration tests that don't share state. 2) Rename files to match the glob pattern. 3) Run bun test and check for race conditions and flaky failures. 4) Continue migrating stable tests file by file.

Run test files concurrently with concurrentTestGlob

The concurrentTestGlob option in [test] runs test files matching a glob pattern with concurrent test execution enabled. For example, concurrentTestGlob = "**/concurrent-*.test.ts" runs matching files concurrently. Test files matching the pattern behave as if --concurrent was passed. The --concurrent CLI flag overrides this setting.

Test execution order

Tests run in the following order: test files run sequentially or across worker processes with --parallel flag, and within each file, tests run sequentially in definition order.

Force a test to run sequentially with test.serial

Use `test.serial()` to force individual tests to run sequentially, even when the `--concurrent` flag is enabled. Example: `test.serial("first serial test", () => { /* ... */ });`

Run tests concurrently within a file

By default, Bun runs all tests sequentially within each test file. Use the `--concurrent` flag to run all tests concurrently: `bun test --concurrent`. Individual tests can be marked with `test.concurrent()` to run in parallel even without the flag.

Mark tests as concurrent with test.concurrent

Use `test.concurrent()` to mark individual tests to run concurrently, even when the `--concurrent` flag is not used: `test.concurrent("concurrent test 1", async () => { /* ... */ });`

Control maximum concurrent tests with --max-concurrency

Use the `--max-concurrency` flag to limit the number of tests running simultaneously: `bun test --concurrent --max-concurrency 4`. The default value is 20.

Default behavior for concurrent test execution

By default, Bun runs all tests sequentially within each test file. Pass `--parallel` to spread test files across CPU cores. When `--concurrent` is enabled, all tests run in parallel unless marked with `test.serial`.

Single process execution for all tests

The test runner runs all tests in a single process by default, providing faster startup, efficient resource usage via shared memory, and simpler debugging. However, this means tests share global state, one test crash can affect others, and there is no true parallelization of individual tests.

--parallel flag runs test files across CPU cores

The --parallel flag runs test files in parallel across worker processes, with N worker processes where N defaults to the number of CPU cores. It can be invoked as 'bun test --parallel' for one worker per CPU core or 'bun test --parallel=4' for exactly 4 workers. The --parallel flag implies --isolate; users can opt out with --no-isolate.

--concurrent flag enables concurrent test execution within a file

The --concurrent flag allows async tests within the same file to overlap while one is awaiting I/O. It can be used as 'test.concurrent(...)' to mark individual tests or 'describe.concurrent(...)' to mark whole groups. The flag --concurrent treats every test as concurrent; test.serial opts back out. The flag --max-concurrency=N caps how many tests run concurrently (default 20).

--shard flag splits test suite across CI machines

The --shard flag runs a deterministic slice of the test suite. Invoked as 'bun test --shard=i/n', it runs the i-th of n slices of the suite. Every machine sorts discovered test files by path and takes a deterministic slice, so together the shards cover each file exactly once with no coordination. Without --timings, file i of the sorted list goes to shard (i mod n) + 1, balanced by file count.

Three independent knobs for test parallelism

Bun test has three independent parallelism mechanisms: --parallel runs test files in processes across CPU cores; --concurrent lets async tests in the same file overlap; --shard splits test files across machines. They compose: a CI job can run 'bun test --shard=2/4 --parallel', and files in that shard can still contain test.concurrent tests.

--isolate runs each test file in a fresh global object

The --isolate flag runs each test file in a fresh JavaScript global object inside the same process. Between files Bun creates a new globalThis, clears the ESM and CommonJS module registries so every file re-evaluates its imports, closes servers/sockets/file watchers/subprocesses left open, cancels timers, and restores fake timers, and re-runs --preload scripts in the new global. This is how Jest and Vitest behave by default.

--parallel implies --isolate by default

--parallel implies --isolate: each file runs in a fresh global object even when two files land on the same worker. Tests that pass with --parallel don't depend on state leaked by an earlier file.

--parallel --no-isolate keeps single global and module registry per worker

--parallel --no-isolate turns off isolation: each worker keeps a single global and module registry for all files it is handed, exactly like a serial 'bun test' does for the whole suite. Each worker evaluates imports (and --preload modules) once instead of once per file, which is the fastest way to run a large suite of small files. The price is that a file can observe whatever an earlier file on the same worker left behind.

Worker environment variables BUN_TEST_WORKER_ID and JEST_WORKER_ID

Each worker in parallel test execution gets BUN_TEST_WORKER_ID and JEST_WORKER_ID set to its 1-based index. Tests can use these environment variables to pick a distinct database, port range, or temp directory per worker, for example: const dbName = `app_test_${process.env.BUN_TEST_WORKER_ID ?? "1"}`

Worker lazy startup and stealing behavior with --parallel

Workers start lazily with --parallel. The first worker starts immediately; the coordinator spawns the rest only once every running worker has been busy for a few milliseconds (--parallel-delay=<ms>, default 5). The coordinator sorts files by path and splits them into one contiguous chunk per worker, so files in the same directory mostly land in the same process. When a worker drains its chunk it steals the back half of the largest remaining chunk from another worker.

--timings flag balances shards by duration instead of file count

The --timings flag provides Bun a record of how long each test file takes, allowing shards to be cut by total time instead of file count. Invoked as 'bun test --timings=.bun-test-timings.json'. With --update-timings, Bun records file durations. The timings file is plain JSON with paths relative to project root and values as wall-clock milliseconds for the whole file. Bun assumes files with no entry take the median time when cutting shards and starts them first under --parallel.

test.concurrent example with async tests

import { test, expect } from "bun:test"; test.concurrent("GET /users", async () => { const res = await fetch(`${baseUrl}/users`); expect(res.status).toBe(200); }); test.concurrent("GET /posts", async () => { const res = await fetch(`${baseUrl}/posts`); expect(res.status).toBe(200); }); test.serial("resets the database", async () => { await resetDb(); });

test.serial opts out of concurrent execution

test.serial marks a test to run serially instead of concurrently. When --concurrent is enabled globally, test.serial opts individual tests back out of concurrent execution so they run one at a time.

Timings file format for --timings flag

The timings file format is plain JSON with the structure: { "version": 1, "files": { "test/integration/build.test.ts": 41234, "test/db/migrate.test.ts": 9876, "src/router.test.ts": 112 } }. Paths are relative to the project root; values are wall-clock milliseconds for the whole file. The file is sorted slowest first, so it doubles as a 'what's slow' report.

--update-timings flag records file durations

The --update-timings flag records how long each test file takes. Without --shard, --update-timings merges into what it read, so re-running part of the suite locally refreshes those entries and keeps the rest. With --shard, --update-timings writes only the files that shard ran, so the shards' outputs are disjoint and can be read together on the next run to add up to the whole suite with no merge step.

Multiple --timings file paths can be passed

You can pass --timings more than once. Bun reads the files as one table and skips paths that don't exist yet. --update-timings writes to the first path only.

Concurrent tests share a thread and global

Concurrent tests share a thread and a global object. This is cooperative concurrency for I/O-bound tests, not extra CPU cores. expect.assertions() and other per-test global state need care under concurrency.

Bun caches transpiled source and bytecode across globals with --isolate

To keep the cost of --isolate low, Bun caches transpiled source and bytecode at the process level and shares them across globals. The second file to import a module skips reading, transpiling and parsing it and goes straight to evaluation. Only the module's top-level code runs again.

Default test execution mode is no isolation with shared global

Without --isolate (the default), all files share one global and one module registry. This is the fastest mode and is fine for suites whose files don't leak state into each other.

Files are distributed by sorting and chunking with --parallel

With --parallel, the coordinator sorts files by path and splits them into one contiguous chunk per worker, so files in the same directory (which usually import the same modules) mostly land in the same process. A chunk boundary can fall inside a directory, and stolen files can move. With --timings, the coordinator cuts chunks by recorded duration instead of file count.

Worker chunks cut by time with --timings and --parallel

With --timings and --parallel, the coordinator cuts the chunks by recorded duration instead of file count, each worker starts its slowest file first, and an idle worker steals the slowest not-yet-started file from whichever chunk has the most time left.

Give your agent this brain