Skip to content

Repository files navigation

macos-fd-raise-limit

Raise macOS file-descriptor / vnode kernel ceilings so a long-running or concurrent Apple Virtualization.framework VM (VirtioFS file sharing) doesn't exhaust the host's file descriptors.

The problem

A VM built on Apple's Virtualization.framework that bind-mounts a host directory over VirtioFS keeps one host file descriptor open per file the guest touches through that share — and does not release them until the VM is torn down. A big dependency/package store (tens of thousands of small files) makes this add up fast: the host's FD and vnode counts climb over the life of a session, and a long-lived or multi-VM workload walks into the kernel caps and runs the host out of descriptors.

This raises those caps. It's a mitigation — descriptors still only free on VM teardown — but with plenty of RAM the ceilings are cheap to raise generously.

The numbers

maxvnodes is the co-limit most people miss. A file descriptor is per-open; a vnode is per-file, shared by every FD pointing at the same file. So N VMs opening the same store cost N× the FDs but only 1× the vnodes — vnode demand tracks the number of distinct files touched. A single heavy VM can brush both the per-process FD cap and the vnode cap at once, so raising maxfilesperproc alone barely helps: vnodes become the new wall. They have to move together.

Defaults measured on Tahoe (macOS 26.5) → target:

sysctl default target × role
kern.maxfilesperproc 245,760 491,520 single-process open-FD cap (one VM helper)
kern.maxvnodes 263,168 1,048,576 ~4× distinct-file cap — the co-limit most people miss
kern.maxfiles 491,520 3,932,160 system-wide FD cap (headroom for ~8 maxed VMs)

Values are tuned generously for a machine with plenty of RAM (e.g. 64 GB); revisit the multipliers for a smaller host. launchctl limit maxfiles (soft 256) is deliberately left alone — the VM helper self-raises its own rlimit well above that, so the kernel caps above are the real walls.

Install

sudo ./install.sh

Idempotent. It installs fd-limits.sh/usr/local/sbin/ (root-owned, so launchd will run it), installs the LaunchDaemon → /Library/LaunchDaemons/, applies the ceilings immediately, then loads the daemon. The runtime sysctls reset on reboot; the daemon (RunAtLoad) re-applies them at every boot, logging to /var/log/fd-limits.log. (install.sh / uninstall.sh also append their own output to a local, gitignored log.)

Verify

The live sysctl values are the source of truth — a daemon showing "loaded" only means it's registered to re-apply at boot, not that its last run succeeded.

sysctl kern.maxfiles kern.maxfilesperproc kern.maxvnodes   # <- the real check
sudo launchctl print system/io.github.jonahbraun.macos-fd-raise-limit | grep -i state
grep achieved /var/log/fd-limits.log | tail -1            # what the last boot actually set

fd-limits.sh logs both the requested and read-back achieved values and exits non-zero if they differ, so a partial apply (e.g. a future macOS rejecting one key) is visible even though the daemon still reports "loaded".

Uninstall

sudo ./uninstall.sh            # remove daemon + script (sysctls revert on reboot)
sudo ./uninstall.sh --revert   # ...and reset the live sysctls to defaults now

Files

file purpose
install.sh idempotent installer (run with sudo)
uninstall.sh remove daemon + script (--revert to reset sysctls)
fd-limits.sh the worker: sets the three sysctls; installed to /usr/local/sbin/, run at boot
io.github.jonahbraun.macos-fd-raise-limit.plist LaunchDaemon that runs the worker at every boot
log local run log — install.sh / uninstall.sh append their output here each time they run (gitignored, created on first run)

About

Raise macOS file-descriptor / vnode kernel ceilings so a VirtioFS-shared Apple Virtualization.framework VM doesn't exhaust the host's file descriptors.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages