cheat sheet

concurrently

Package-level reference for concurrently on npm — install, output prefixing, kill-others, race-mode, and the npm-run-all2 comparison.

concurrently

What it is

concurrently runs multiple npm scripts in parallel and merges their output with optional name prefixes and colour. It's the most-used solution for "start the server AND the bundler watcher in one command", and it works cross-platform (bash and PowerShell both choke on & in different ways).

Originally written in 2015 by Kimmo Brunfeldt, it's stable, dependency-light, and ships an enormous battery of UX knobs: kill-others-on-fail, race-mode (first-to-finish wins), restart on exit, raw passthrough, custom prefix templates.

Install

bash
# Almost always a dev dep
npm install -D concurrently
pnpm add -D concurrently
yarn add -D concurrently

# One-off
npx concurrently "npm:watch" "npm:server"

Output: concurrently (and shorthand concurrent, conc) binaries on PATH under node_modules/.bin/.

Versioning & Node support

  • Current major line is 9.x — released late 2024. Drops Node 16 support, modernises the TS internals, refines --raw and --kill-others semantics.
  • 8.x (2023) is widely deployed.
  • Recent releases require Node 18+.
  • Always a dev dependency; never ships in runtime artefacts.

Package metadata

  • Maintainer: Kimmo Brunfeldt (initial author), Gustavo Henke (current).
  • Project home: github.com/open-cli-tools/concurrently
  • npm: npmjs.com/package/concurrently
  • License: MIT
  • First released: 2015
  • Downloads: ~10-15 million per week. Most JavaScript dev workflows have it in devDependencies even if they don't use it directly — it's a transitive dep of many scaffolds.

Peer dependencies & extras

concurrently has a handful of small dependencies (chalk, rxjs, tree-kill, yargs) and no peer deps. The bundle is ~50 KB minified. No common companion packages — it's a leaf utility.

Adjacent toolPurpose
npm-run-all2Sequential / parallel script runner — script-glob support is its main differentiator
tsx / ts-nodeWhat you actually run through concurrently — TS execution
nodemonAuto-restart on file change — often paired with concurrently
wait-onWait for a port/file/URL before continuing — pair with concurrently to start a server, wait, then run tests

Alternatives

ToolTrade-off
npm-run-all2Glob-based script matching (npm-run-all build:*), sequential by default with run-s / parallel with run-p. More terse for many-script chains.
Bash &npm run a & npm run b — works on Unix, breaks on Windows. Best fallback when concurrently is unwanted.
makeParallel via -j N; cross-platform if you ship make (POSIX-required in WSL/CI).
turbo runMonorepo-aware; runs across packages with topological awareness and remote caching. Overkill for single-package script orchestration.
pnpm -r runpnpm's built-in workspace script runner — sequential by default.

Common gotchas

  1. Shell quoting hell. concurrently "npm run a" "npm run b" works; concurrently npm run a npm run b does NOT (all four are positional args). Always quote each script.
  2. npm: prefix for named-prefix lookup. concurrently "npm:watch:*" matches every script starting with watch: and uses the suffix as the prefix label. Without npm:, it shells out the literal command.
  3. Exit code defaults to the first failure with --kill-others-on-fail, else 0. Bare concurrently exits 0 even if one script crashed. Add --kill-others-on-fail (or --kill-others) for CI.
  4. --raw disables prefix injection. Combined with --no-color, you get pristine subprocess output — useful when piping to a log aggregator that has its own structure.
  5. Signals are forwarded best-effort. SIGINT propagates to children, but child cleanup is up to the child. nodemon, tsx, and webpack-dev-server all handle it; bespoke scripts may leak processes.
  6. tree-kill is used on Windows. Killing a child also kills its descendants. On macOS/Linux, default behaviour is the same but uses POSIX signal groups.
  7. Output interleaving. Two scripts writing to stdout in parallel will interleave at line boundaries. Prefixes help; for fully separated streams use --output-prefix + a JSON logger per child.

Real-world recipes

Run multiple watchers

The canonical dev-server scenario: bundle, type-check, and serve in parallel.

json
// package.json
{
  "scripts": {
    "dev": "concurrently \"npm:dev:*\"",
    "dev:server": "node --watch server.js",
    "dev:client": "vite",
    "dev:types": "tsc --watch --noEmit"
  }
}
bash
npm run dev

Output:

text
[dev:server] Server listening on http://localhost:3000
[dev:client] VITE v5.0.0 ready in 245 ms
[dev:client] ➜ Local: http://localhost:5173/
[dev:types] Found 0 errors. Watching for file changes.

Named output prefixes with colours

-n (or --names) names each script; -c (or --prefix-colors) colours them.

bash
npx concurrently -n "API,WEB,TYPES" -c "blue,green,yellow" \
  "npm run dev:server" "npm run dev:client" "npm run dev:types"

Output:

text
[API]   listening on 3000
[WEB]   ready in 245 ms
[TYPES] Found 0 errors.

For glob matches, prefixes come from the script name automatically:

bash
npx concurrently "npm:dev:*"
# Prefixes: [server], [client], [types]

Output:

text
[server] api listening on 4000
[client] vite ready in 312 ms
[types]  Found 0 errors.

Kill-others on first failure

By default, one script crashing doesn't stop the others. For CI / test:all aggregation, that's wrong — fail fast:

bash
npx concurrently --kill-others-on-fail "npm:lint" "npm:typecheck" "npm:test"

Output:

text
[lint] ✓ no issues
[typecheck] ✗ error TS2304: Cannot find name 'foo'
--> Sending SIGTERM to other processes..
[test] EXITED via SIGTERM

Exit code is the first non-zero child's code, propagated. Use --kill-others (no -on-fail) if you also want a manual SIGINT to kill all peers.

Race mode — first script to finish wins

Useful for "one of these strategies should produce a result first":

bash
npx concurrently --kill-others --success first \
  "npm run fetch:from-cache" "npm run fetch:from-network"

Output:

text
[0] fetched from cache in 12 ms
--> Sending SIGTERM to other processes..
[1] EXITED via SIGTERM

--success first means concurrently exits 0 as soon as any one child exits 0; the rest are killed.

Wait-for-port pattern

Use wait-on (separate package) to wait for the server before running tests:

json
{
  "scripts": {
    "test:e2e": "concurrently -k -s first \"npm:dev\" \"npm:test:cypress\"",
    "test:cypress": "wait-on http://localhost:3000 && cypress run"
  }
}

-k (kill-others) + -s first (succeed on first exit) means: when Cypress finishes, kill the dev server and propagate Cypress's exit code.

Restart on exit

The --restart-tries N --restart-after MS flags re-launch a child that exits — handy for flaky watchers:

bash
npx concurrently --restart-tries 3 --restart-after 1000 "npm:dev:server"

Output:

text
[0] server crashed: ECONNREFUSED
[0] restart attempt 1/3 in 1000ms
[0] server listening on 3000

Per-child env

concurrently doesn't have a built-in per-child env flag. Use shell prefixes:

json
{
  "scripts": {
    "dev": "concurrently \"PORT=3000 npm:dev:api\" \"PORT=5173 npm:dev:web\""
  }
}

On Windows, use cross-env: concurrently \"cross-env PORT=3000 npm:dev:api\" ….

Production deployment

concurrently is a dev tool. Never ship it in dependencies — for production multi-process orchestration use:

  • pm2 for Node process management
  • Docker Compose for multi-service local dev
  • Kubernetes / Nomad for production fleets
  • systemd for VM-based deployments

The one production-adjacent use: in a CI job that needs to run db migrate && (start-server & run-tests) cross-platform.

Performance tuning

concurrently has near-zero overhead — it spawns each child, multiplexes output via streams, and that's it. The only real knob:

  • --raw skips the prefix logic — minor speedup, more importantly preserves ANSI colour codes from child output for terminals that need them.
  • --no-color for log files — strips concurrently's own colours; child colours still come through unless they detect non-TTY.

For very large numbers of parallel children (>20), prefer npm-run-all2 or a Makefile — concurrently's UX scales poorly past ~10 prefixes.

Version migration guide

From → ToHighlights
6 → 7New TypeScript internals. RxJS upgraded. Some --prefix template changes.
7 → 8Node 14 dropped. tree-kill upgraded. Better Windows kill semantics.
8 → 9Node 16 dropped. --kill-others-on-fail semantics clarified (was ambiguous in 8).

Drop-in usage

Migration is usually painless — the CLI flags have been stable since v6. Read the changelog for the rare cases where --prefix templates changed.

Security considerations

  1. Children inherit env. process.env (including secrets) is passed to every child. Strip with env -i or cross-env --no-env if you need a clean child env.
  2. Untrusted script args. concurrently "$USER_INPUT" is shell injection. Never accept untrusted strings as concurrently arguments.
  3. --passthrough-args (v9+) forwards trailing CLI args to children. This is convenient but means a CI argument can affect every script — review usage.
  4. No sandboxing. Children run with the same uid/gid as concurrently. For untrusted code use containers, not concurrently.

Configuration patterns

Via package.json

The dominant style. Everything is a script string:

json
{
  "scripts": {
    "dev": "concurrently -k -n WEB,API -c green,blue \"npm:dev:web\" \"npm:dev:api\"",
    "dev:web": "vite",
    "dev:api": "tsx watch src/server.ts"
  }
}

Via JSON config file

Lesser-used; pass --config pointing at a JSON file. Useful when the command list is long enough to be unreadable inline:

json
// concurrently.json
{
  "commands": [
    { "name": "web",   "command": "npm run dev:web",   "prefixColor": "green" },
    { "name": "api",   "command": "npm run dev:api",   "prefixColor": "blue" },
    { "name": "types", "command": "npm run dev:types", "prefixColor": "yellow" }
  ],
  "killOthersOnFail": true
}
bash
npx concurrently --config ./concurrently.json

Output:

text
[web]   vite ready in 245 ms
[api]   listening on 4000
[types] Found 0 errors.

Troubleshooting common errors

  • "concurrently: command not found" — not installed, or node_modules/.bin not on PATH. npx concurrently … works regardless.
  • Scripts run but no prefixes show--raw is set somewhere; or the child writes to process.stderr only, which concurrently passes through differently.
  • One child won't die on SIGINT — the child ignores or eats the signal. concurrently --kill-others uses tree-kill (SIGKILL fallback after 1 s); set --kill-signal SIGTERM for graceful shutdown if your child handles it.
  • Output interleaves at non-line boundaries — child is not line-buffering. Force line-buffering with stdbuf -oL <cmd> on Linux/macOS.
  • "Exit code 0 even though one script failed" — missing --kill-others-on-fail. Add it for CI use.

Ecosystem integrations

  • CI configs — concurrently runs identically in GH Actions, GitLab CI, CircleCI, etc. No platform-specific quirks.
  • turbo run dev — Turborepo invokes per-package dev scripts in parallel; many of those scripts internally use concurrently for sub-processes.
  • nx run-many --target=dev — Nx similarly. Sub-orchestration via concurrently is common.
  • Docker Compose — for multi-service dev, Docker Compose replaces concurrently entirely. Many teams keep concurrently for "fast local without Docker" and Compose for "full-stack with services".

When NOT to use this

  • A single watcher. Just run the script directly — no orchestration needed.
  • Production multi-process management. Use pm2, forever, systemd, Docker Compose, or Kubernetes — concurrently has no health checks, no graceful restart, no log rotation.
  • Many script names matching a glob pattern. npm-run-all2 (run-p build:*) is more terse for that case.
  • Cross-package monorepo orchestration. Use Turborepo or Nx — they handle topological deps and remote caching that concurrently can't.
  • You need health checks, port waiting, retries. Pair concurrently with wait-on, wait-port, or pm2 — concurrently has minimal lifecycle features.

concurrently shines in the "two-to-five watchers in dev mode" sweet spot. Outside that, reach for a more specialised tool.

See also