How to run Jest tests in parallel cloud sandboxes
Install the project once, fork the sandbox into copies, give each copy one jest --shard=i/n, and merge the JUnit files afterwards.
On Runtime the install happens once, not once per shard. A fork copies a
running sandbox with its files, memory and processes, so every copy starts with
node_modules already in place. The source pauses only for the moment the
copy takes, about a second for a fresh sandbox. Four shards of a 2 vCPU, 4 GiB sandbox cost $0.32 an hour in all with
every CPU busy, and Runtime bills the CPU the tests use, not the CPUs held.
Jest 30.5.2 was the current release on 25 September 2026.
Why split across machines
Jest runs its tests in worker processes. In a single run the default is the number of cores minus one, so on a 2 vCPU sandbox Jest uses one worker. A larger suite goes faster spread over several sandboxes than squeezed into one:
| Layout | Jest workers | Where the time goes |
|---|---|---|
| One 2 vCPU sandbox | 1 | Every test file, one after another |
One sandbox, --maxWorkers=2 |
2 | Two files at once, sharing two CPUs |
Four sandboxes, --shard |
4 (1 each) | A quarter of the files on each machine |
--shard=2/4 picks the second of four slices of the test files. Every shard
sees the same list, so together they run each file once.
Install once, then fork
TypeScriptimport { mkdir } from "node:fs/promises";import { Sandbox } from "withruntime";const shards = 4;await using base = await Sandbox.create({ timeoutSeconds: 1800 });await base.files.upload("./web", "/workspace/web");await base.exec( "cd web && npm ci --no-fund --no-audit && npm install --no-save jest-junit@17.0.0", { check: true, timeoutMs: 600_000, },);const copies = await base.fork({ count: shards - 1 });const machines = [base, ...copies];const runs = await Promise.all( machines.map((sbx, i) => sbx.exec( `cd web && npx jest --ci --shard=${i + 1}/${shards} --reporters=default --reporters=jest-junit`, { env: { JEST_JUNIT_OUTPUT_DIR: "reports", JEST_JUNIT_OUTPUT_NAME: `shard-${i + 1}.xml` }, timeoutMs: 900_000, }, ), ),);await mkdir("reports", { recursive: true });await Promise.all( machines.map((sbx, i) => sbx.files.download(`/workspace/web/reports/shard-${i + 1}.xml`, `reports/shard-${i + 1}.xml`), ),);await Promise.all(copies.map((copy) => copy.stop()));runs.forEach((run, i) => { if (run.exitCode !== 0) console.error(`shard ${i + 1} failed\n${run.stderr}`);});process.exitCode = runs.every((run) => run.exitCode === 0) ? 0 : 1;The Python client does the same with AsyncRuntime, so the shards run at once:
Pythonimport asyncioimport osfrom withruntime import AsyncRuntimeSHARDS = 4async def main() -> int: async with AsyncRuntime() as runtime: async with await runtime.sandboxes.create(timeout_seconds=1800) as base: await base.files.upload("./web", "/workspace/web") await base.exec("cd web && npm ci --no-fund --no-audit && npm install --no-save jest-junit@17.0.0", check=True, timeout_ms=600_000) copies = await base.fork(count=SHARDS - 1) machines = [base, *copies] async def shard(sbx, i): run = await sbx.exec( f"cd web && npx jest --ci --shard={i}/{SHARDS} --reporters=default --reporters=jest-junit", env={"JEST_JUNIT_OUTPUT_DIR": "reports", "JEST_JUNIT_OUTPUT_NAME": f"shard-{i}.xml"}, timeout_ms=900_000) await sbx.files.download(f"/workspace/web/reports/shard-{i}.xml", f"reports/shard-{i}.xml") return run.exit_code os.makedirs("reports", exist_ok=True) codes = await asyncio.gather(*(shard(sbx, i + 1) for i, sbx in enumerate(machines))) await asyncio.gather(*(copy.stop() for copy in copies)) return 0 if all(code == 0 for code in codes) else 1raise SystemExit(asyncio.run(main()))- The copies: each is a sandbox of its own, the same size as the source and billed like one. Stop them when their shard is done. A free trial runs eight sandboxes at once, which covers this four-way split.
- The reports:
jest-junitwrites one XML file per shard. Most CI systems and test dashboards read a folder of JUnit files as one run. --ci: Jest then fails a test whose snapshot is missing instead of writing a new one. For code an agent wrote, that stops a new snapshot from passing just because it was recorded on the spot.
Coverage from every shard
Add --coverage to each shard and download its coverage folder under a
separate name, such as coverage/shard-1. Each shard covers only its own
files, so merge the reports with your coverage tool before reading a total.
Bake node_modules into an image
For a suite that runs many times a day, build the dependencies into a
custom image from a Dockerfile. Copy the lockfile before the
source: a rebuild then starts from the checkpoint after npm ci and reruns only
the steps after the second COPY, so a code change does not reinstall
anything.
dockerfileFROM runtimeWORKDIR /workspace/webCOPY package.json package-lock.json ./RUN npm ci --no-fund --no-audit && npm install --no-save jest-junit@17.0.0COPY . .RUN chown -R 1000:1000 /workspace/webPut node_modules in the project's .dockerignore so your own machine's copy
stays out of the build context. Then build and run one shard per sandbox:
TypeScriptimport { readFile } from "node:fs/promises";import { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({ name: "web-tests", dockerfile: await readFile("web/Dockerfile.test", "utf8"), contextDir: "web",});const shards = 4;const runs = await Promise.all( Array.from({ length: shards }, async (_, i) => { await using sbx = await runtime.sandboxes.create({ image: "web-tests" }); return await sbx.exec(`cd web && npx jest --ci --shard=${i + 1}/${shards}`, { timeoutMs: 900_000, }); }),);process.exitCode = runs.every((run) => run.exitCode === 0) ? 0 : 1;The fork approach needs no image and suits one-off runs, such as checking the change an agent just made. The image suits a suite you run on every commit.
Facts that shape a Jest run
| Fact | Value |
|---|---|
| Node.js in the image | 24.21.0; Jest 30 supports Node 18.14, 20, 22 and 24 or later |
| Command timeout | 60 seconds unless you pass timeoutMs; at most 24 hours |
| Output kept in a result | 64 KiB of each stream, all of it when you stream with onStdout |
| Forks per call | 1 to 10 running copies |
| Sandboxes at once | 8 on the trial, 100 on a paid account to start |
Related
- pytest suites in a sandbox
- Vitest feedback for a coding agent
- What a sandbox fork is
- Run JavaScript and Node.js code in a sandbox
- Snapshots and forks
Sources
Checked 25 September 2026.
- Jest CLI options:
--shard,--ci,--maxWorkers(cores minus one in single-run mode) - jest on npm: version 30.5.2, Node
^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0 - jest-junit:
--reporters=jest-junit,JEST_JUNIT_OUTPUT_DIR,JEST_JUNIT_OUTPUT_NAME; version 17.0.0 on npm
Facts on this page were checked on 25 September 2026.