Gateway
Gateway lock
Why
- Only one gateway process should own a state directory; run additional gateways with isolated profiles, state directories, configs, and ports.
- Survive crashes/SIGKILL without leaving stale lock files behind.
- Fail fast with a clear error when another gateway already owns the port.
Three layers
Startup enforces ownership in three steps, in order:
- State ownership lock acquires a lock keyed by the canonical state directory. Every Gateway participates, including Gateways started with
OPENCLAW_ALLOW_MULTI_GATEWAY=1, so destructive SQLite maintenance cannot race a live owner. - Config lock acquires the historical per-config lock and records the runtime port. Multi-Gateway mode skips this config singleton but retains the state ownership lock.
- Socket bind binds the HTTP/WebSocket listener (default
ws://127.0.0.1:18789) as an exclusive TCP listener.
Each layer can fail independently and throws its own GatewayLockError.
State and config locks
-
Lock files, SQLite coordinators, and transient reclaim guards live under
$OPENCLAW_STATE_DIR/tmp/openclaw-<uid>(oropenclawon platforms without a user ID). An overridden state directory therefore owns its complete lock tree. -
Lock liveness comes from the recorded PID, platform process start identity when available, and Gateway process identity. A verified owner remains authoritative during startup before its port begins listening.
-
A dedicated SQLite coordinator serializes metadata inspection, stale-owner reclamation, and lock replacement. Its exclusive transaction is released automatically if the owning process crashes.
-
If a lock file is missing or the recorded owner process is gone, startup reclaims the lock and continues.
-
If either lock is actively held, startup retries for up to 5 seconds (default) before giving up:
text GatewayLockError("gateway already running (pid <pid>); lock timeout after <ms>ms")
Socket bind
-
On
EADDRINUSE, startup retries the bind for up to 20 attempts at 500ms intervals (roughly 10 seconds total) to ride out aTIME_WAITwindow after a recently exited process. -
If the port is still in use after retries:
text GatewayLockError("another gateway instance is already listening on ws://127.0.0.1:<port>") -
Other bind failures:
text GatewayLockError("failed to bind gateway socket on ws://127.0.0.1:<port>: <cause>")
On shutdown, the gateway closes the HTTP/WebSocket server and removes its state and config lock files.
The state-local layout is a clean version boundary. Binaries from before this change use the process temp directory, so an old and new binary sharing one state directory during an upgrade do not exclude each other through these locks.
Operational notes
- If the port is occupied by a different, non-gateway process, the error is the same; free the port or choose another with
openclaw gateway --port <port>. OPENCLAW_ALLOW_MULTI_GATEWAY=1permits multiple config/runtime instances, not shared mutable state. Each instance still needs a uniqueOPENCLAW_STATE_DIR.- Under a service supervisor, a new gateway process that hits either error above first probes
/healthzon the existing process. If that process is healthy, the new process leaves it in control instead of failing. On systemd, it exits with code78; the unit'sRestartPreventExitStatus=78stopsRestart=alwaysfrom looping on a lock orEADDRINUSEconflict. If the existing process never becomes healthy, the health-probe retry is time-bounded and startup then fails with the lock error above instead of looping forever. - The macOS app keeps its own lightweight PID guard before spawning the gateway; the file lock and socket bind above are the actual runtime enforcement.
Related
- Multiple Gateways - running multiple instances with unique ports
- Troubleshooting - diagnosing
EADDRINUSEand port conflicts