Running Concurrent Processes

How to run multiple commands in parallel with output streaming

Running Concurrent Processes

This guide covers common patterns for running multiple processes concurrently.

Basic Usage

import { runConcurrently } from "@visulima/task-runner";

const result = await runConcurrently([
    "npm run build",
    "npm run test",
    "npm run lint",
]);

if (!result.success) {
    console.error("Some commands failed:");
    for (const event of result.closeEvents) {
        if (event.exitCode !== 0) {
            console.error(`  ${event.name ?? event.command}: exit ${event.exitCode}`);
        }
    }
}

Command Shortcuts

Use parseCommands to expand package manager shorthand before passing to runConcurrently:

import { parseCommands, runConcurrently } from "@visulima/task-runner";

const commands = parseCommands([
    "npm:build",     // npm run build
    "pnpm:test",     // pnpm run test
    "yarn:lint",     // yarn run lint
    "bun:dev",       // bun run dev
    "node:script",   // node --run script
    "deno:check",    // deno task check
]);

await runConcurrently(commands);

Note: runConcurrently does not expand shortcuts internally. Always use parseCommands first if you need shortcut or wildcard expansion.

Wildcard Expansion

Expand commands against package.json scripts using parseCommands:

import { parseCommands, runConcurrently } from "@visulima/task-runner";

// If package.json has: { "scripts": { "watch-js": "...", "watch-css": "...", "watch-tests": "..." } }
const commands = parseCommands(["npm:watch-*"]);
// Result: [{ command: "npm run watch-js" }, { command: "npm run watch-css" }, ...]

await runConcurrently(commands);

// Deno tasks from deno.json are also supported:
const denoCommands = parseCommands(["deno:dev-*"]);

Real-Time Output Streaming

Stream stdout/stderr as lines arrive:

await runConcurrently(
    ["npm run build", "npm run test"],
    {
        onEvent: (event) => {
            switch (event.kind) {
                case "stdout":
                    console.log(`[${event.index}] ${event.text}`);
                    break;
                case "stderr":
                    console.error(`[${event.index}] ${event.text}`);
                    break;
                case "close":
                    console.log(`[${event.index}] exited with ${event.exitCode}`);
                    break;
                case "error":
                    console.error(`[${event.index}] ${event.message}`);
                    break;
            }
        },
    },
);

Controlling Parallelism

Limit the number of simultaneous processes:

// Run at most 2 processes at a time
await runConcurrently(
    ["build-a", "build-b", "build-c", "build-d"],
    { maxProcesses: 2 },
);

Kill Others on Failure

Stop all processes when one fails:

await runConcurrently(
    ["npm run typecheck", "npm run lint", "npm run test"],
    {
        killOthers: ["failure"],  // Kill all if any exits non-zero
        killTimeout: 3000,        // Wait 3s, then SIGKILL
    },
);

Running Dev Servers

For long-running processes, use stdin: "inherit" so the child can read keyboard input:

await runConcurrently([
    { command: "vite dev", name: "frontend", stdin: "inherit" },
    { command: "node server.js", name: "backend" },
]);

// Ctrl+C sends SIGINT to all process groups
// Exit codes are translated to 0 (user cancellation is not failure)

Retry Failed Commands

Automatically restart failed commands with configurable delay:

// Fixed delay
await runConcurrently(["flaky-build"], {
    restart: { tries: 3, delay: 1000 },  // 3 retries, 1s between each
});

// Exponential backoff (1s, 2s, 4s, 8s, ...)
await runConcurrently(["flaky-build"], {
    restart: { tries: 5, delay: "exponential" },
});

Cleanup After Completion

Run teardown commands sequentially after all processes finish:

await runConcurrently(
    ["npm run dev", "docker compose up"],
    {
        teardown: [
            "docker compose down",
            "rm -rf .cache",
        ],
    },
);

Timing Summary

Print a timing table after all commands complete:

await runConcurrently(
    ["npm run build", "npm run test", "npm run lint"],
    { timings: true },
);

// Output:
// ── Timing Summary ───────────────────────────────────
//
// name  │ duration │ code │ killed │ command
// ──────┼──────────┼──────┼────────┼────────────────────
// test  │ 5.2s     │ 0    │ no     │ npm run test
// build │ 3.1s     │ 0    │ no     │ npm run build
// lint  │ 1.8s     │ 0    │ no     │ npm run lint

Custom Shell

Override the shell used for command execution:

// Auto-detected from npm config (npm config set script-shell)
await runConcurrently(["echo hello"]);

// Explicit override
await runConcurrently(["echo hello"], {
    shellPath: "C:\\Program Files\\Git\\bin\\bash.exe",
});

// Direct execution (no shell)
await runConcurrently([
    { command: "node build.js", shell: false },
]);

Argument Placeholders

Pass arguments to commands via placeholders:

import { parseCommands } from "@visulima/task-runner";

const commands = parseCommands(
    ["echo {1} {2}", "echo {@}", "echo {*}"],
    { additionalArguments: ["hello", "world"] },
);

// Results:
// echo 'hello' 'world'    ← {1} and {2} replaced individually
// echo 'hello' 'world'    ← {@} = all args, individually quoted
// echo 'hello world'      ← {*} = all args as single string

Named Commands

Give commands meaningful names for logging and success conditions:

const result = await runConcurrently(
    [
        { command: "npm run build", name: "build" },
        { command: "npm run test", name: "test" },
        { command: "npm run lint", name: "lint" },
    ],
    {
        // Only "test" needs to pass
        successCondition: "command-test",
    },
);
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues