--- name: osx-tuning description: Tune macOS resource limits and sysctls for best performance with Go development, Docker/OrbStack, and Linux VMs. Use this skill whenever the user asks to "tune macOS", "raise file descriptor limits", "fix ulimit", "optimize for Docker/Go/VM performance", "increase maxfiles/maxproc/somaxconn", or reports EMFILE/too-many-open-files, connection refused under load, or slow container/VM networking. Covers the full chain: launchd maxfiles, per-shell ulimit, and boot-time sysctl tuning via LaunchDaemons, with verification and revert steps. --- # macOS Performance Tuning (Go / Docker / Linux VMs) Raise macOS resource limits and tune TCP sysctls for heavy local development: Go servers, Docker/OrbStack containers, and Linux VMs. Everything here is reversible and verified. ## Why the defaults are low macOS ships conservative soft limits. The two that bite developers: - **`maxfiles` soft limit = 256** — set by launchd for every process. Go's `net/http` servers, Docker, and build tools open far more than 256 file descriptors. Hitting it surfaces as `EMFILE: too many open files` or `socket: too many open files`. - **`kern.ipc.somaxconn = 128`** — max pending TCP connections per listen socket. Go servers and the Docker API socket queue up behind this under load. The hard limits are already `unlimited`/high, so nothing is truly capped — the soft values just need raising. ## The three layers Tuning must happen at three levels because each only affects a subset of processes: | Layer | Affects | Mechanism | |---|---|---| | launchd `maxfiles` | all new processes | LaunchDaemon plist | | per-shell `ulimit` | interactive shells | `~/.zshrc` | | sysctls | kernel-wide, all processes | LaunchDaemon plist | ## 1. launchd maxfiles (system-wide) Create `/Library/LaunchDaemons/limit.maxfiles.plist`: ```xml Label limit.maxfiles ProgramArguments launchctl limit maxfiles 65536 unlimited RunAtLoad ServiceIPC ``` Install and load: ```bash sudo cp /tmp/limit.maxfiles.plist /Library/LaunchDaemons/limit.maxfiles.plist sudo chown root:wheel /Library/LaunchDaemons/limit.maxfiles.plist sudo chmod 644 /Library/LaunchDaemons/limit.maxfiles.plist sudo launchctl load -w /Library/LaunchDaemons/limit.maxfiles.plist ``` Verify: ```bash launchctl limit maxfiles # expect: maxfiles 65536 unlimited ``` ## 2. Per-shell ulimit (interactive shells) The launchd change only affects **newly launched** processes. Already-open shells keep their old soft limit, so add a line to `~/.zshrc`: ```zsh # Raise open file descriptor soft limit (hard limit is unlimited) ulimit -n 65536 ``` Takes effect in new shells, or `source ~/.zshrc` in open ones. ## 3. Boot-time sysctls macOS does not reliably honor `/etc/sysctl.conf` on modern versions. Use a LaunchDaemon that runs `sysctl -w` at boot. Create `/Library/LaunchDaemons/com.local.sysctl.plist`: ```xml Label com.local.sysctl ProgramArguments /bin/sh -c sysctl -w kern.maxprocperuid=4000 kern.ipc.somaxconn=1024 net.inet.tcp.sendspace=262144 net.inet.tcp.recvspace=262144 net.inet.tcp.win_scale_factor=5 net.inet.tcp.mssdflt=1460 RunAtLoad ServiceIPC ``` Install and load (same pattern as above, target `com.local.sysctl.plist`). ### What each sysctl does | sysctl | Default | Tuned | Why | |---|---|---|---| | `kern.maxprocperuid` | 2666 | 4000 | Max processes per user. Go goroutines, Docker/VM threads, build workers all count. 2666 is low for heavy dev. | | `kern.ipc.somaxconn` | 128 | 1024 | Max pending TCP connections per listen socket. Go `net/http` and the Docker API socket hit this under load. | | `net.inet.tcp.sendspace` | 131072 | 262144 | Larger TCP send buffer — better throughput for Go servers and VM/container networking. | | `net.inet.tcp.recvspace` | 131072 | 262144 | Larger receive buffer, same reason. | | `net.inet.tcp.win_scale_factor` | 3 | 5 | Enables larger TCP window scaling — matters for high-bandwidth transfers (Docker pulls, VM traffic). | | `net.inet.tcp.mssdflt` | 512 | 1460 | Default MSS. 512 is the conservative fallback; 1460 matches Ethernet MTU 1500, reducing packet overhead. | ## Verification ```bash launchctl limit maxfiles # maxfiles 65536 unlimited ulimit -n # 65536 in a new shell sysctl kern.maxprocperuid kern.ipc.somaxconn \ net.inet.tcp.sendspace net.inet.tcp.recvspace \ net.inet.tcp.win_scale_factor net.inet.tcp.mssdflt sudo launchctl list | grep -iE "sysctl|maxfiles" # both loaded, exit 0 ``` ## Reverting ```bash sudo launchctl unload -w /Library/LaunchDaemons/limit.maxfiles.plist sudo rm /Library/LaunchDaemons/limit.maxfiles.plist sudo launchctl unload -w /Library/LaunchDaemons/com.local.sysctl.plist sudo rm /Library/LaunchDaemons/com.local.sysctl.plist # remove the ulimit line from ~/.zshrc ``` ## Notes / caveats - **`kern.maxproc` (4000) and `kern.maxfiles` (2147483647)** are already at their caps — no change needed. - **`net.inet.ip.forwarding`** should stay 0 unless you're building a router. - **`vm.overcommit`** is not a macOS knob; memory is managed by the VM/compressor. High swap usage is normal compressed-memory behavior, not a leak. - **Docker/OrbStack VM CPU/RAM** is configured in OrbStack's settings GUI, not via sysctl — that's where you bump resources for the Linux VM itself. - **`win_scale_factor` and `mssdflt`** are the most opinionated changes. Safe on a modern network, but revert those first if you ever see odd TCP behavior. - **`sudo` is required** for the LaunchDaemon installs and `sysctl -w`. The sandbox blocks localhost listeners and some privileged ops — run these outside a sandbox if a step fails with a permission error.