Summary
On a headless Mac mini, omlx serve hangs immediately after the startup banner when spawned directly by launchd — via omlx start / brew services start omlx (the homebrew.mxcl.omlx job) or a hand-written LaunchAgent. The server process stays alive (~0% CPU, blocked in select), never reaches Application startup complete, and never binds the port. The same omlx serve run in an interactive shell or a tmux session starts normally (binds, loads the model, serves) within seconds.
Separately, a hung/old server process doesn't terminate cleanly: a stale LISTEN socket lingers on the port and a leftover omlx-server process makes subsequent instances hang at the banner. Only force-killing all omlx-server PIDs + freeing the port lets a fresh instance start.
Environment
- omlx 0.4.3 (Homebrew,
jundot/omlx tap)
- macOS 26.5.1 (25F80), Mac mini
Mac16,10 / Apple M4 / 24 GB
- Headless (SSH/Screen Sharing); user is auto-logged-in at the console (a GUI session exists)
model_dir on an external SSD; model mlx-community/gemma-4-12B-it-qat-4bit (~10.7 GB)
Repro
omlx start (or brew services start omlx, or a LaunchAgent running omlx serve).
curl http://127.0.0.1:<port>/v1/models → connection refused / HTTP 000, even though lsof -iTCP:<port> -sTCP:LISTEN may show a socket.
- Service log shows only the banner (
oMLX … Version: 0.4.3), never Application startup complete.
sample <omlx-server pid> → alive, ~0% CPU, blocked in select.
- Same command in a shell:
tmux new-session -d -s omlx 'omlx serve' → starts fully, binds, serves. ✅
Notes / hypotheses
Startup (uvicorn bind / engine-worker spawn) seems to depend on something present in an interactive/pty context but absent under a bare launchd job (controlling TTY?). Setting PYTHONUNBUFFERED, explicit PATH/HF_HOME in the LaunchAgent env, and a login-shell exec wrapper did not help. The non-clean shutdown + phantom LISTEN socket may relate to #352 (bound-but-unresponsive). Related: #833 (headless auto-start without login).
Workaround
Run omlx serve inside a tmux session (so it runs in a pty, not spawned directly by launchd), launched by a launchd agent, with an HTTP /v1/models health check (the open socket alone is unreliable) and force-kill-then-restart on failure.
Summary
On a headless Mac mini,
omlx servehangs immediately after the startup banner when spawned directly by launchd — viaomlx start/brew services start omlx(thehomebrew.mxcl.omlxjob) or a hand-written LaunchAgent. The server process stays alive (~0% CPU, blocked inselect), never reachesApplication startup complete, and never binds the port. The sameomlx serverun in an interactive shell or atmuxsession starts normally (binds, loads the model, serves) within seconds.Separately, a hung/old server process doesn't terminate cleanly: a stale
LISTENsocket lingers on the port and a leftoveromlx-serverprocess makes subsequent instances hang at the banner. Only force-killing allomlx-serverPIDs + freeing the port lets a fresh instance start.Environment
jundot/omlxtap)Mac16,10/ Apple M4 / 24 GBmodel_diron an external SSD; modelmlx-community/gemma-4-12B-it-qat-4bit(~10.7 GB)Repro
omlx start(orbrew services start omlx, or a LaunchAgent runningomlx serve).curl http://127.0.0.1:<port>/v1/models→ connection refused / HTTP 000, even thoughlsof -iTCP:<port> -sTCP:LISTENmay show a socket.oMLX … Version: 0.4.3), neverApplication startup complete.sample <omlx-server pid>→ alive, ~0% CPU, blocked inselect.tmux new-session -d -s omlx 'omlx serve'→ starts fully, binds, serves. ✅Notes / hypotheses
Startup (uvicorn bind / engine-worker spawn) seems to depend on something present in an interactive/pty context but absent under a bare launchd job (controlling TTY?). Setting
PYTHONUNBUFFERED, explicitPATH/HF_HOMEin the LaunchAgent env, and a login-shellexecwrapper did not help. The non-clean shutdown + phantomLISTENsocket may relate to #352 (bound-but-unresponsive). Related: #833 (headless auto-start without login).Workaround
Run
omlx serveinside atmuxsession (so it runs in a pty, not spawned directly by launchd), launched by a launchd agent, with an HTTP/v1/modelshealth check (the open socket alone is unreliable) and force-kill-then-restart on failure.