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.
$ 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:
-
lan→0.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. -
loopback→127.0.0.1only — 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, but127.0.0.1and 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:
-
openclaw gateway statusreports how the gateway bound to interfaces. If it shows the Tailscale IP rather than0.0.0.0, you've found it. - Read the listen socket from the kernel:
awk '$4=="0A" && $2 ~ /:4965$/' /proc/net/tcp:4965is hex for port18789. A local address of00000000:4965is0.0.0.0:18789(healthylanbind); a non-zero hex local address means it bound to a specific interface — the bug. - Curl both addresses. If
curl 127.0.0.1:18789/healthis refused butcurl http://<your-tailnet-ip>:18789/healthreturns200, 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.
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 handles stale
.jsonl.lockfiles, 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.
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 --followhangs or times out reaching the gateway — a CLI handshake timeout, a different reachability failure: see whyopenclaw logs --followtimes 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 thetailnetdrift 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.
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.