Search intent: you run a pnpm/npm workspaces-style tree but want Bun for install speed or scripting while one package (or a legacy subtree) still expects npm and package-lock.json. On remote Mac CI behind cross-border links, the failure mode is rarely “wrong CPU”—it is double resolution, stale mirrors, and retry loops that mask lockfile drift. This page is a decision matrix: pick a single lockfile authority, align registry mirror env for both tools, cap concurrent installs, name cache keys from the right hashes, and set retry thresholds that stop at integrity errors. Start from the MacPull homepage or the technical blog index; for adjacent JS package-manager matrices see Yarn Berry PnP vs node_modules and Deno / JSR cross-border pulls—all readable without logging in.

Executable decision matrix (hybrid workspace + cross-border registry)

Use the table as a merge gate: every row should have an owner and a value in your pipeline YAML or runbook before you scale concurrent jobs on shared Apple Silicon hosts.

Split the problem in two layers: per-job settings (npm sockets, Bun cache path, frozen installs) and per-builder admission control (how many concurrent pipelines hit the same egress IP and disk). On multi-tenant remote Mac pools, the second layer usually dominates—two “modest” jobs that each open twelve sockets can still trigger 429 storms on a shared mirror VIP. Track error codes per hour and tighten job concurrency before you micro-optimize tarball deduplication.

Decision Pick A Pick B Remote Mac CI note (2026)
Lockfile authority Bun.lockb only; ban npm install at repo root; use bun install --frozen-lockfile package-lock.json only; Bun runs with --no-save or not used for deps Never let both tools regenerate locks in CI. Binary Bun.lockb (or text bun.lock on newer Bun) is PR-hostile for human diff—gate on checksum + install log artifacts instead of line-by-line lock review.
Registry mirror Single upstream via NPM_CONFIG_REGISTRY + scoped //registry.npmjs.org/:_authToken in CI secrets Approved regional mirror + allowlist in bunfig.toml [install].registry mirroring npm policy Bun respects npm-style config; mismatched registry between npm ci and bun install produces divergent trees—pin one URL per job.
Concurrent install Bun default parallelism; cap simultaneous jobs per Mac (queue depth 1–2 for heavy monorepos) npm config set maxsockets 12 (start 8–16; measure 429 rate) Cross-border RTT: raising concurrency past ~24 often increases 429s without improving p50 install time.
Cache key naming bun-<os>-<hash(bun.lockb|bun.lock)>-<hash(package.json**)> npm-<os>-<hash(package-lock.json)>-<node>-<npm> Include Node major and npm or Bun semver in the key when the tool version affects resolution or optional deps.
Failure retry threshold Network: max 3 attempts, backoff 2s / 4s / 8s Lockfile or integrity: max 0 retries (fail job immediately) Teach orchestrators to grep logs for ERESOLVE, frozen-lockfile, checksum and skip outer retry wrappers for those tokens.

Bun.lockb versus package-lock.json in one repo

Bun.lockb encodes the graph Bun resolved; package-lock.json encodes npm’s arborist layout including lockfileVersion quirks. If a subdirectory must stay on npm for publishing or legacy postinstall scripts, isolate it: separate package.json root with its own lockfile and run npm ci only there, while the workspace root uses Bun. Document the boundary in README so reviewers do not “fix” drift with the wrong tool.

Acceptance checks that survive audits: (1) CI prints the Bun and npm versions from the same job metadata block. (2) Frozen installs: bun install --frozen-lockfile at root and npm ci in npm-only packages. (3) Optional dependencies: compare npm_config_platform / npm_config_arch with what Bun reports on macOS arm64 so native binaries resolve consistently.

Copy-paste environment variables and commands

Set these at the start of the install phase (shell or CI “env” block). Replace mirror hostnames with the endpoint your security team approved.

# Cross-border friendly registry + TLS patience (both npm and Bun read npm-style config)
export NPM_CONFIG_REGISTRY="https://registry.npmjs.org/"   # or your approved mirror URL
export NPM_CONFIG_FETCH_RETRIES="3"
export NPM_CONFIG_FETCH_RETRY_MINTIMEOUT="20000"
export NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT="120000"
export NPM_CONFIG_MAXSOCKETS="12"

# Bun install cache location (helps shared warm runners; use per-job dirs on multi-tenant pools)
export BUN_INSTALL_CACHE_DIR="${CI_CACHE_DIR:-$HOME/.cache/bun}/install"

# Optional: keep npm cache next to Bun cache for hybrid jobs
export npm_config_cache="${CI_CACHE_DIR:-$HOME/.cache}/npm"

# --- Frozen installs (pick the lane that matches your authority row) ---
bun --version
bun install --frozen-lockfile

# npm-only subtree example:
# (cd packages/legacy-npm && npm ci --omit=dev)

Commit a bunfig.toml next to the root package.json when you want repo-local defaults (still no secrets in git):

# bunfig.toml (example — adjust registry)
[install]
# registry = "https://your.approved.mirror.example/npm"
frozenLockfile = true
# optional: reduce install-time script risk in CI
# ignoreScripts = true
  1. Mirror URL is identical in NPM_CONFIG_REGISTRY, .npmrc committed for scopes, and bunfig.toml if you set [install].registry.
  2. Private @org/* scopes use //npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN} (or your vault) with read-only tokens.
  3. Job-level NODE_OPTIONS=--max-old-space-size=8192 only after measuring; memory pressure plus parallel installs OOMs Mac agents before the registry times out.

CI cache key snippets (GitHub Actions style)

Names matter more than compression: a bad key either poisons the tree with stale optional deps or thrashes the CDN on every commit.

# Example: split caches so Bun binary lock does not invalidate npm cache
- uses: actions/cache@v4
  with:
    path: ~/.bun/install/cache
    key: bun-${{ runner.os }}-${{ hashFiles('bun.lockb', 'bun.lock') }}-${{ hashFiles('**/package.json') }}
    restore-keys: |
      bun-${{ runner.os }}-${{ hashFiles('bun.lockb', 'bun.lock') }}-

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-node${{ matrix.node }}-

Bounded retry shell (network only)

Wrap transport failures; exit immediately when the log shows frozen lockfile or integrity errors.

#!/usr/bin/env bash
set -euo pipefail
attempt=1 max=4 delay=2
log=bun-install-$attempt.log
while [ "$attempt" -le "$max" ]; do
  log="bun-install-${attempt}.log"
  if bun install --frozen-lockfile 2>&1 | tee "$log"; then exit 0; fi
  if grep -qiE 'frozen.?lock|lockfile|checksum|integrity|ERESOLVE' "$log"; then exit 1; fi
  sleep "$delay"; delay=$((delay * 2)); attempt=$((attempt + 1))
done
exit 1

FAQ

Should developers run bun install and npm install interchangeably? No. Pick one command for day-to-day lock updates per package root; use the other only in documented maintenance windows with two-person review.

Does a faster mirror remove the need for concurrency caps? No. Mirrors still rate-limit; caps protect shared remote Mac egress and keep p95 queue time predictable.

Where do Yarn-only docs fit? If part of the monorepo still uses Yarn, treat it as a third island with its own cache key and lockfile gate—or standardize. See the Yarn Berry matrix linked above.

Summary

Hybrid Bun + npm works on remote Mac CI when one lockfile authority owns each package root, both tools read the same registry mirror URL, concurrent install stays in the 8–24 socket band until metrics say otherwise, cache keys hash the correct lock artifacts plus toolchain versions, and retries stop at the first integrity or lockfile error. That combination cuts cross-border flakes without hiding supply-chain drift.

For Apple Silicon build capacity sized to your registry strategy, open macpull.com (homepage, no login), review pricing and purchase options, and read the help center before you scale runner concurrency. When you are ready to compare other JS ecosystem pulls, return to the blog list for the Yarn and Deno articles cited in the introduction.