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:

  1. 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.
  2. 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.
  3. 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> (or openclaw on 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 a TIME_WAIT window 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=1 permits multiple config/runtime instances, not shared mutable state. Each instance still needs a unique OPENCLAW_STATE_DIR.
  • Under a service supervisor, a new gateway process that hits either error above first probes /healthz on the existing process. If that process is healthy, the new process leaves it in control instead of failing. On systemd, it exits with code 78; the unit's RestartPreventExitStatus=78 stops Restart=always from looping on a lock or EADDRINUSE conflict. 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.
Was this useful?
On this page

On this page