Runtime

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-junit writes 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/web

Put 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

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.