npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cpu-mutex

v0.3.0

Published

Machine-wide mutex for heavy commands — one CPU-saturating run at a time across every project, via the kernel's own file lock (lockf/flock). No stale locks, ever.

Readme

cpu-mutex

Machine-wide mutex for heavy commands — one CPU-saturating run at a time across every project on the machine.

Built for fleets of AI agents (or humans) working in parallel checkouts that share one CPU pool: concurrent full test suites and builds starve each other into timeout failures that carry no information about any diff. Wrap the heavy commands and they queue instead.

// package.json — in every project on the machine
{
  "scripts": {
    "test": "cpu-mutex -- vitest run",
    "build:full": "cpu-mutex -- turbo build"
  }
}

The default lock file lives outside any repo (~/Library/Application Support/cpu-mutex/ on macOS, $XDG_STATE_HOME/cpu-mutex/ or ~/.local/state/cpu-mutex/ on Linux, %LOCALAPPDATA%\cpu-mutex\ on Windows), so unrelated projects contend on the same lock out of the box — that is the feature.

Why a kernel lock

The mutex is the kernel's own: lockf (macOS/BSD) or flock (util-linux) hold an advisory lock on a file descriptor, which the kernel releases when the holder ends — however it ends, SIGKILL and crashes included. That removes the entire stale-lock problem (heartbeats, liveness probes, age ceilings, steal protocols) rather than solving it. There is no lock to clean up, ever.

Zero runtime dependencies; both utilities ship with their OS.

Behavior

  • A blocked run waits and says so on stderr: who holds the lock (pid, start time, cwd) and how to inspect it (lsof <lockfile>). Waiting is normal, not a hang.
  • Waiting is bounded (--wait / CPU_MUTEX_WAIT_S, default 30 min). Past the ceiling the run proceeds unserialized and prints running WITHOUT the lock — that line always means the run is unprotected, never that things worked quietly.
  • Running can be bounded too (--timeout, opt-in, no default). The wait ceiling protects the queue; it does nothing about the holder itself — a wedged run keeps the lock and a core until someone notices. With --timeout the clock starts when the command actually starts (queue time never counts); on expiry the whole process group gets SIGTERM, then SIGKILL after 5 s, and the wrapper exits only once the group is empty (giving up 10 s after SIGTERM — only a member in uninterruptible sleep outlives SIGKILL) — the lock utility dies first, and reporting at that point would leave a SIGTERM-ignoring command running with the lock already free. During the grace the lock is already released, so the next waiter may start beside the dying run. Exit 124 (SIGTERM sufficed) or 137 (SIGKILL was needed), timeout(1)'s convention, with a stderr line naming the timeout. GNU timeout(1) nested inside (cpu-mutex -- timeout 600 tsc) bounds the run the same way, but it moves itself into its own process group — out of reach of the wrapper's group kill, so signalling the wrapper frees the lock while timeout and the command keep running. It is also absent on stock macOS. There is deliberately no environment variable: a run ceiling belongs to one command, and a machine-wide value would land on commands of very different lengths. A descendant that left the process group (setsid) survives the kill.
  • Exit codes pass through untouched. A red run is reported red exactly once — a startup sentinel distinguishes "the command ran and failed" from "the locking utility failed", so a utility failure never silently re-runs your suite.
  • Signals kill the whole run. The command runs in its own process group; SIGINT/SIGTERM/SIGHUP/SIGQUIT to the wrapper take the group down before the lock is released. Without this, killing the wrapper would free the lock while the command keeps burning every core — the exact parallel run the mutex exists to prevent.
  • CI is off by default (any non-empty CI): one runner, no contention, and a lock must never hang a job. CPU_MUTEX=1 overrides.
  • Degradation is loud, never silent: no utility on the system, an unwritable lock directory, or an exhausted wait all run the command unlocked and say so on stderr.
  • Junk configuration cannot disable the mutex. An invalid CPU_MUTEX_WAIT_S (a typo, 2.5, 1e21, a value past 2^31−1) is ignored with a notice and the default is used — validated on the token, because the utility would reject the string and the rejection would otherwise read as "could not acquire". An unrecognized CPU_MUTEX value (true, yes) is likewise reported before falling back to the default behavior.

CLI

cpu-mutex [--name <lock>] [--wait <seconds>] [--timeout <seconds>] -- <command> [args...]
cpu-mutex --status [--name <lock>]
  • --name <lock> — a separate named lock ([A-Za-z0-9._-]+). Distinct names never contend; use them to serialize different resource classes independently. Default: default.

  • --wait <seconds> — wait ceiling before running unlocked (positive integer, max 2^31−1).

  • --timeout <seconds> — run ceiling, counted from the command's actual start (positive integer, max 2147483 — Node's timer range). Exits 124/137 on expiry; see Behavior.

  • --status — who holds the mutex right now, without taking it:

    $ cpu-mutex --status
    ~/Library/Application Support/cpu-mutex/default.lock: held by pid 4321 since 2026-09-09T11:02:07.311Z — /Users/me/proj (vitest run)

    The held/free verdict comes from the kernel (the same non-blocking probe a waiting run uses); the pid/cwd/command line is the holder's advisory sidecar and is only shown when the kernel agrees the lock is held. Scriptable exit codes: 0 free, 1 held, 2 usage error, 3 cannot tell (no locking utility). For the full picture including waiters: lsof <lockfile>.

The -- may be omitted when the command does not itself start with a dash: cpu-mutex vitest run works.

Environment

| Variable | Effect | | ------------------ | ---------------------------------------------------------------------- | | CPU_MUTEX=0 | force off (beats everything) | | CPU_MUTEX=1 | force on (beats CI) | | CI (non-empty) | off by default | | CPU_MUTEX_WAIT_S | wait ceiling in seconds (default 1800) | | CPU_MUTEX_FILE | explicit lock file path (overrides --name and the state dir) | | CPU_MUTEX_DIR | directory for named locks (default: the OS state dir, machine-wide) | | CPU_MUTEX_BIN | the locking utility, overriding the lockf/flock PATH search |

Programmatic API

import { runLocked, lockFilePath } from 'cpu-mutex';

const code = await runLocked(['vitest', 'run'], { name: 'default', waitS: 900 });
process.exit(code);

runLocked(argv, opts) spawns the command under the lock and resolves to its exit code. Options: name (named lock), file (explicit lock file, overrides derivation), waitS (wait ceiling — invalid values throw a TypeError, unlike env values which fall back loudly), timeoutS (run ceiling, same semantics and validation as --timeout), env (environment to read CPU_MUTEX*/CI/PATH configuration from; the command itself always inherits the real process.env).

On SIGINT/SIGTERM/SIGHUP/SIGQUIT it kills the process group of every live runLocked command in the process (concurrent calls share one signal handler) and exits the process. If your application must survive signals, wrap the call in a subprocess.

Also exported: lockStatus({ name?, file?, env? }) — the --status answer as data ({ file, busy: boolean | null, holder? }); lockFilePath(name?, env?) — the derived lock file path (throws on an invalid name); LOCK_NAME_RE, MAX_WAIT_S — the validation bounds.

There is deliberately no acquire()/release() or withLock(fn) API: the lock's lifetime is tied to a process holding a descriptor, which is exactly what makes it stale-proof.

What it does NOT protect against

Honesty about scope, so the gaps are known rather than discovered:

  • Resident CPU consumers. A daemon (language server, MCP server, watcher) never takes a mutex; a single uncontended run can still starve under one.
  • Projects serializing through their own mechanism. A lock-by-file-existence scheme pointed at this lock's path would not merge the two mutexes — its stale-steal would unlink the inode this lock lives on and break mutual exclusion silently. Sharing requires both sides on this package (or at least on the same kernel primitive and path).
  • Descendants that leave the process group. The lock lives exactly as long as the lockf/flock process; the command never holds the descriptor (close-on-exec on macOS, flock -o on Linux). Group kills are what tie the command's life to the lock, so a descendant that detached into its own session (setsid) keeps running after the lock is released.
  • A wedged holder. A live process keeps the lock for as long as it lives. The wait ceiling bounds the queue behind it; only --timeout bounds the holder itself, and it is opt-in.
  • Windows. No lockf/flock — runs execute unlocked, loudly.