Blog

OpenClaw: gateway is listening, but localhost says "connection refused"

The OpenClaw gateway logs ready and "http server listening," but curl http://127.0.0.1:18789 — or the Control UI, the desktop app, a reverse proxy, or a health probe — gets connection refused. The gateway really is up; it's just listening on the wrong interface. The one setting that decides this is gateway.bind: lan listens on 0.0.0.0 (everything, including loopback), loopback on 127.0.0.1 only, and tailnet on the Tailscale IP only — so a tailnet bind is "up" while 127.0.0.1 is refused. The fix, in one line: set gateway.bind to "lan" in your openclaw.json and restart the gateway.

What you'll see
$ curl http://127.0.0.1:18789/health
curl: (7) Failed to connect to 127.0.0.1 port 18789: Connection refused

# ...even though the gateway log says:
gateway ready — http server listening

What gateway.bind actually controls

gateway.bind picks the network interface the gateway's HTTP/WebSocket server listens on. The value is one of three modes, and the difference is exactly which addresses can reach it:

  • lan0.0.0.0 — every interface the host has, including loopback (127.0.0.1) and the tailnet IP. It's a superset: localhost, your LAN, and Tailscale all work at once. This is the OpenClaw default.
  • loopback127.0.0.1 only — localhost clients work, but nothing off-box (LAN, Tailscale, another container) can connect.
  • tailnet → the Tailscale IP only (e.g. 100.x.y.z:18789) — remote tailnet peers work, but 127.0.0.1 and the LAN are refused.

So "gateway up, but 127.0.0.1 refused" is almost always bind: "tailnet" (or the equivalent gateway.tailscale.mode: "serve", which pins the listener to the Tailscale interface). The gateway is healthy — it's just not on the address your client is using.

Why the gateway is "up" but localhost is refused

The value usually drifts to tailnet for one of two reasons: you (or a control-UI toggle) set it while wiring up remote access over Tailscale, or you enabled gateway.tailscale.mode: "serve" and the gateway bound specifically to the Tailscale interface IP instead of 0.0.0.0. Either way the listener moves off loopback, and every localhost consumer breaks at once: the Control UI on localhost:18789, the CLI/desktop app talking to the local gateway, a reverse proxy in front of it, and any health check that curls 127.0.0.1. Upstream reports of the gateway showing "running" while a local client can't connect track this same interface mismatch (openclaw/openclaw#43381, #21519).

Confirm it before you change anything

Don't guess at firewalls and port conflicts — read the actual listen address first. Three ways, fastest to most direct:

  1. openclaw gateway status reports how the gateway bound to interfaces. If it shows the Tailscale IP rather than 0.0.0.0, you've found it.
  2. Read the listen socket from the kernel:
    awk '$4=="0A" && $2 ~ /:4965$/' /proc/net/tcp
    :4965 is hex for port 18789. A local address of 00000000:4965 is 0.0.0.0:18789 (healthy lan bind); a non-zero hex local address means it bound to a specific interface — the bug.
  3. Curl both addresses. If curl 127.0.0.1:18789/health is refused but curl http://<your-tailnet-ip>:18789/health returns 200, the gateway is fine and the bind is the problem. That split is the signature of this issue.

Fix it (self-hosted)

Set the bind back to lan in the live config and bounce the gateway:

# in ~/.openclaw/openclaw.json
"gateway": {
  "bind": "lan",   // was "tailnet" or "loopback"
  ...
}

Then restart the gateway (openclaw gateway stop && openclaw gateway start, or send the process a reload). Loopback, the LAN, and Tailscale all reach it again, because 0.0.0.0 is a superset of all three.

The trap: the wrong value comes back after a restart

gateway.reload.mode defaults to hybrid, which persists runtime and Control-UI changes back to the on-disk config. So if tailnet was written at runtime, it's now saved in your live openclaw.json — and a plain process or pod restart just reloads the same wrong value. Editing a ConfigMap or a baked default doesn't help either if the live file already holds tailnet. You have to change gateway.bind in the actual config file the gateway reads, then restart. If tailnet keeps reappearing after that, something is rewriting the config at runtime (a control-UI action or a sync job) — hunt that down rather than re-editing the file each time.

Keep it safe after switching to lan

lan binds 0.0.0.0, so the gateway is now reachable on every interface the host has — which is the whole point, but it means you must not run it open. OpenClaw already requires a gateway auth token for any non-loopback bind (gateway.auth.token), so keep that set, and put a firewall or (on Kubernetes) a NetworkPolicy in front of port 18789 if the box is internet-reachable. Pure tailnet binding is not the right way to restrict exposure here — it can't satisfy a loopback health probe, whose target is hard-wired to 127.0.0.1. Use lan plus a token and a firewall instead.

Stop babysitting your OpenClaw box

Fix it once — or stop fixing it for good.

Apply the checklist above and keep self-hosting, or skip the maintenance entirely: run your OpenClaw on managed hosting from $6.90/mo, starting with a 7-day free trial. We handle the stale locks, gateway restarts, version upgrades, and uptime — and you can import your existing instance in a couple of minutes. Cancel anytime.

Managed hosting — from $6.90/mo Your own hosted OpenClaw instance with automatic restarts and version upgrades. Starts with a 7-day free trial — import your current setup, keep your channels, cancel anytime.
$199 managed setup — optional Prefer we do it for you? One workspace configured end-to-end: first-run config, one 30-minute onboarding/debug session, and a 7-day follow-up. Limited weekly slots.
  • Managed hosting handles stale .jsonl.lock files, gateway restarts, and version upgrades for you
  • Import your existing OpenClaw setup in minutes — keep your channels and configuration
  • The optional $199 setup is scoped: no custom development, enterprise/SRE support, or unsupported self-hosting repair

If you would rather compare options first, review OpenClaw cloud hosting or see the best OpenClaw hosting options before deciding.

OpenClaw import first screen in OpenClaw Setup dashboard (light theme) OpenClaw import first screen in OpenClaw Setup dashboard (dark theme)
1) Paste import payload
OpenClaw import completed screen in OpenClaw Setup dashboard (light theme) OpenClaw import completed screen in OpenClaw Setup dashboard (dark theme)
2) Review and launch

Related, but not this

  • The gateway answers, but rejects your browser with "origin not allowed" — that's the Control-UI CORS allow-list, not the bind interface: see how to fix OpenClaw "origin not allowed".
  • openclaw logs --follow hangs or times out reaching the gateway — a CLI handshake timeout, a different reachability failure: see why openclaw logs --follow times out.
  • You're on macOS and bind: "lan" or "auto" still only listens on localhost — that's a separate platform-specific bug (openclaw/openclaw#4947), not the tailnet drift this page is about.

How managed hosting avoids this

The bind interface, the auth token, and the firewall in front of the gateway are all decisions you have to get right — and keep right when reload.mode: hybrid quietly rewrites the config under you. On managed OpenClaw hosting from Lobsterland, the gateway is generated with bind: "lan" and a token by default, and the config lifecycle is owned by the platform, so a stray tailnet value can't strand your instance with a gateway that's "up" but unreachable. You connect over the dashboard; the interface binding is not your problem to debug.

Import your current OpenClaw instance in 1 click

Frequently asked questions

Why is my OpenClaw gateway listening but localhost says connection refused?

Because the gateway is listening on a different interface than the one your client is using. gateway.bind decides which: lan is 0.0.0.0 (every interface, including loopback), loopback is 127.0.0.1 only, and tailnet is the Tailscale IP only. If bind is tailnet (or gateway.tailscale.mode is serve), the gateway is up on the Tailscale IP but not on 127.0.0.1, so anything hitting localhost gets connection refused. Set gateway.bind to "lan" and restart the gateway.

What does gateway.bind do in OpenClaw (lan vs loopback vs tailnet)?

gateway.bind chooses the network interface the gateway listens on. lan binds 0.0.0.0 — a superset that covers loopback and the tailnet IP at once. loopback binds 127.0.0.1 only, so nothing off-box can reach it. tailnet binds only the Tailscale interface IP, so loopback and the LAN are refused. The default OpenClaw (and Lobsterland) generates is lan, which is why localhost, the Control UI, and a health probe all work at once.

Why does the wrong gateway.bind value come back after I restart the gateway?

Because gateway.reload.mode defaults to hybrid, which persists runtime and Control-UI changes back to the on-disk openclaw.json. If tailnet was written at runtime, it's now saved in the live config and survives a process or pod restart — the restart reloads the same wrong value. You have to edit gateway.bind in the actual config file (not just restart) and then bounce the gateway.

Is it safe to set gateway.bind to lan (0.0.0.0)?

Yes, as long as the gateway auth token stays set and the port isn't openly reachable. lan binds 0.0.0.0, so the gateway is exposed on every interface the host has. OpenClaw already requires a token for non-loopback binds (gateway.auth.token), so keep that configured, and put a firewall or NetworkPolicy in front if the box is internet-reachable. Don't fix connection-refused by binding wide with auth disabled.

How do I see which interface my OpenClaw gateway actually bound to?

Run openclaw gateway status, which reports how the gateway bound to interfaces. To read it straight from the kernel, check the listen socket: awk '$4=="0A" && $2 ~ /:4965$/' /proc/net/tcp — local address 00000000:4965 means 0.0.0.0:18789 (healthy), while a non-zero hex address means it bound to a specific interface like the tailnet IP. The signature of this bug is curl 127.0.0.1:18789/health refused while a curl against the tailnet IP returns 200.

Cookie preferences