Gateway Lock: Ensuring Single Instance with File and Port Guards

Learn how Gateway enforces single ownership through file locks and TCP binding. Essential for operators running multiple gateways or handling crash recovery.

Read this when

  • Running or debugging the gateway process
  • Investigating single-instance enforcement

Why

  • A state directory must be claimed by exactly one gateway process. To run additional gateways, give each one its own profile, state directory, config, and port.
  • Crashes and SIGKILL must not leave behind stale lock files.
  • If another gateway already holds the port, startup should stop immediately with a clear error message.

Three layers

Ownership is enforced at startup through three sequential layers:

  1. State ownership lock takes a lock tied to the canonical state directory. This applies to every Gateway, including those launched with OPENCLAW_ALLOW_MULTI_GATEWAY=1, preventing destructive SQLite maintenance from running concurrently with a live owner.
  2. Config lock takes the legacy per-config lock and notes the runtime port. In Multi-Gateway mode, this config singleton is skipped, but the state ownership lock still applies.
  3. Socket bind attaches the HTTP/WebSocket listener (default ws://127.0.0.1:18789) as an exclusive TCP listener.

Each of these layers can fail on its own and raises its own GatewayLockError.

State and config locks

  • Lock files, SQLite coordinators, and transient reclaim guards are stored under $OPENCLAW_STATE_DIR/tmp/openclaw-<uid> (or openclaw when the platform lacks a user ID). Thus, overriding the state directory gives it a complete lock tree of its own.

  • Lock liveness relies on the recorded PID, the platform's process start identity where available, and the Gateway process identity. A confirmed owner stays authoritative during startup until its port begins listening.

  • A dedicated SQLite coordinator handles metadata inspection, stale-owner reclamation, and lock replacement in a serialized manner. If the owning process dies, its exclusive transaction is released automatically.

  • When a lock file is absent or its recorded owner process no longer exists, startup reclaims the lock and proceeds.

  • If either lock is held by an active process, startup retries for up to 5 seconds (default) before abandoning the attempt:

    GatewayLockError("gateway already running (pid <pid>); lock timeout after <ms>ms")
    

Socket bind

  • On EADDRINUSE, startup attempts the bind again up to 20 times, waiting 500ms between tries (about 10 seconds in total), to get past a TIME_WAIT window following a recently exited process.

  • If the port remains occupied after all retries:

    GatewayLockError("another gateway instance is already listening on ws://127.0.0.1:<port>")
    
  • For other bind failures:

    GatewayLockError("failed to bind gateway socket on ws://127.0.0.1:<port>: <cause>")
    

When shutting down, the gateway closes the HTTP/WebSocket server and deletes its state and config lock files.

The state-local layout marks a clean version boundary. Binaries predating this change rely on the process temp directory, so during an upgrade an old and a new binary sharing one state directory do not block each other through these locks.

Operational notes

  • If a non-gateway process holds the port, the error looks the same; release the port or pick a different one via openclaw gateway --port <port>.
  • OPENCLAW_ALLOW_MULTI_GATEWAY=1 allows multiple config/runtime instances, but not shared mutable state. Each instance still requires its own OPENCLAW_STATE_DIR.
  • Under a service supervisor, a fresh gateway process that encounters either error above first checks /healthz on the running process. If that process is healthy, the new process leaves it in charge rather than failing. On systemd, it exits with code 78; the unit's RestartPreventExitStatus=78 keeps Restart=always from retrying endlessly on a lock or EADDRINUSE conflict. If the existing process never becomes healthy, the health-probe retry has a time limit, after which startup fails with the lock error above instead of looping indefinitely.
  • The macOS app maintains its own lightweight PID guard before launching the gateway; the file lock and socket bind above provide the actual runtime enforcement.
617 words · updated Aug 10, 2026