Gateway Rate Limits: Auth, Webhooks, Control-Plane, ACP, Restart

Reference for every Gateway rate limit: pre-auth lockouts, browser and webhook throttles, control-plane write caps, ACP session limits, and restart cooldown. Essential for operators configuring security.

Read this when

  • A client sees `rate limit exceeded for <method>`, `AUTH_RATE_LIMITED`, or lockout errors
  • You want to tune `gateway.auth.rateLimit`
  • You are reasoning about brute-force protection on an exposed Gateway
  • You need to know which Gateway surfaces are throttled, at what limits

The Gateway applies several independent rate limits. Each one guards a distinct boundary, relies on a different identity for keying, and produces its own error format. This page documents all of them.

Overview:

SurfaceLimit (default)Keyed byConfigurable
Failed auth (token/password/device)10 failures / 60s, 5 min lockoutIP + credential scopegateway.auth.rateLimit
Browser-origin WS auth failuressame, loopback not exemptIP, or page origin from loopbackgateway.auth.rateLimit
Webhook (/hooks) auth failures20 failures / 60s, 60s lockoutIPno
Control-plane write RPCs30 requests / 60s per methodmethod + device + IPno
ACP session creation120 sessions / 10stranslator instanceinternal
Gateway restart cycles30s cooldown between restartsprocessno

Authentication attempts (pre-auth)

Before any request processing begins, failed authentication attempts are throttled per client IP. This acts as the brute-force defense for Gateways exposed to the network.

  • Only incorrect credentials consume budget. Clients that send no token at all, along with successful authentications, do not count against the limit. A successful auth normally resets the counter for the matching credential class on that client IP; succeeding with a device token does not clear failures tied to shared secrets, and the reverse also holds.
  • The defaults are 10 failures within 60 seconds, followed by a 5 minute lockout for that IP.
  • Loopback (127.0.0.1 / ::1) is excluded by default, so local CLI sessions cannot get locked out.
  • Counters are separated by credential class, meaning a burst against one surface will not affect another. These classes cover the shared gateway token/password, device tokens, node pairing, paired-node reapproval, device bootstrap tokens, and watchOS challenge issuance.

While a lockout is active, connection attempts are refused with:

{
  "code": "INVALID_REQUEST",
  "message": "unauthorized: too many failed authentication attempts (retry later)",
  "retryable": true,
  "retryAfterMs": 297000,
  "details": {
    "code": "AUTH_RATE_LIMITED",
    "authReason": "rate_limited",
    "recommendedNextStep": "wait_then_retry"
  }
}

During the lockout, attempts coming from other resolved IPs, including direct loopback, remain unaffected.

Adjust this under gateway.auth.rateLimit in openclaw.json:

{
  "gateway": {
    "auth": {
      "rateLimit": {
        "maxAttempts": 10,
        "windowMs": 60000,
        "lockoutMs": 300000,
        "exemptLoopback": true
      }
    }
  }
}

Repeated AUTH_RATE_LIMITED lines in the Gateway log indicate someone is attempting to guess credentials; consult the exposure runbook for guidance.

Browser-origin connections

WebSocket connections carrying a browser Origin header follow the same limits, but the loopback exemption is always disabled for them. A malicious page running in a local browser is still an untrusted client, so localhost receives no special treatment on this path. When such a connection originates from a loopback address, its failures are bucketed by the normalized page origin (for instance browser-origin:https://evil.example) rather than the shared loopback IP, giving each origin its own counter. For non-loopback addresses, the client IP remains the key. This behavior cannot be changed.

Unconfigured same-host reverse proxies

If a request arrives through a loopback socket carrying forwarding headers, but the proxy is not listed in gateway.trustedProxies, OpenClaw cannot safely assign the request to the claimed forwarded IP. Gateway-authenticated routes reject the request before checking credentials or fallback auth. HTTP requests get 403 with error type proxy_attribution_required; WebSocket auth returns the same reason along with configuration advice. Registered plugin-authenticated webhook routes may process the request using their own signature or credential policy, but they disregard forwarded client claims and apply the non-exempt socket source for pre-auth limits.

Set the proxy address narrowly in gateway.trustedProxies and ensure the proxy overwrites or safely reconstructs forwarding headers. OpenClaw then restores validated per-client attribution and rate-limit buckets. Refer to Trusted Proxy Auth and the Gateway security guide for details.

A headerless TCP forwarder offers no request-level provenance and looks identical to a process connecting directly over loopback. This hardening does not treat that transport as a proxy. Do not rely on a same-host TCP forwarder as a remote-access security boundary; instead use managed Tailscale, SSH, or an HTTP reverse proxy configured as described above.

OpenClaw-managed Tailscale Serve and Funnel operate through a separate private loopback listener. Reaching that listener establishes the managed ingress path, and Tailscale's rewritten source address selects a normal non-exempt, resettable per-client bucket. Serve tokenless identity auth additionally requires a matching WhoIs result; Funnel requires its marker and password authentication.

An externally managed Serve or Funnel route pointed at the ordinary Gateway listener can establish generic proxy attribution only when its immediate source is explicitly configured in gateway.trustedProxies and it provides a valid non-loopback forwarded client address. OpenClaw then uses that client address for rate limits and applies normal gateway auth; Tailscale headers do not grant managed-ingress or tokenless-auth semantics. Without that trust configuration, Gateway-authenticated routes reject the unattributable ingress. Prefer gateway.tailscale.mode: "serve" or "funnel" when OpenClaw should own the route and its dedicated listener.

Webhooks

The HTTP /hooks ingress has its own failure limiter: 20 failed authentications per 60 seconds per client IP, followed by a 60 second lockout. Loopback is not exempt. Successful hook auth resets the counter. Throttled requests receive plain HTTP 429 Too Many Requests with a Retry-After header (seconds). Limits are fixed; if a legitimate integration trips this, correct its credentials rather than retrying harder.

Control-plane writes (post-auth backstop)

Write-side admin RPCs (config.apply, config.patch, plugins.install, plugins.setEnabled, plugins.uninstall, update.run, worktrees.*, gateway.restart.request, ...) are additionally rate-limited after authorization: 30 requests per 60 seconds, per method, per deviceId+clientIp.

This is not a security boundary, callers already hold operator.admin, but rather a backstop that limits runaway client or agent loops hammering expensive operations. Interactive use never reaches it; each method has its own bucket, so toggling a plugin does not consume the budget of config writes.

When exceeded, the request fails with a retryable error:

{
  "code": "UNAVAILABLE",
  "message": "rate limit exceeded for config.patch; retry after 35s",
  "retryable": true,
  "retryAfterMs": 34539,
  "details": { "method": "config.patch", "limit": "30 per 60s" }
}

Clients should honor retryAfterMs. The limit is fixed (not configurable); buckets expire on their own and are pruned by Gateway maintenance.

ACP session creation

The ACP translator caps session creation at 120 new sessions per 10 second window per translator instance. Exceeding it fails the request with an error whose message carries the wait time (there is no structured retryAfterMs field on this path):

ACP session creation rate limit exceeded for <method>; retry after <n>s.

This bounds runaway clients that create sessions in a loop; normal IDE and agent use stays far below it.

Restart cooldown

Gateway restart requests coalesce, then enforce a 30 second cooldown between restart cycles. A restart requested during the cooldown is scheduled after it expires rather than rejected. This is separate from the control-plane limiter above: gateway.restart.request consumes a control-plane budget slot and the resulting restart obeys the cooldown.

Operational notes

  • All limiters are in-memory and per-process, and multiple Gateways do not share state. Replacing the Gateway process clears the Gateway-owned counters (auth lockouts, webhook throttle, control-plane buckets). The restart cooldown deliberately survives in-process restart cycles, that is what it throttles, and resets only with the process. The ACP session cap belongs to its translator instance and resets when that instance is recreated, not on Gateway restart.
  • Bucket maps are bounded (hard entry caps plus periodic pruning), so unique-key floods cannot grow memory without bound.
  • When a client is behind a reverse proxy, the effective IP is the resolved client IP. An unconfigured loopback proxy is rejected until its address and header-rebuilding behavior are trusted explicitly. See trusted proxy auth for how proxy headers are validated before they can influence attribution.
  • Retry signaling varies by surface: Gateway RPC limiters return retryable: true plus retryAfterMs, the webhook ingress uses HTTP 429 with a Retry-After header, and ACP embeds the wait in the error message. In every case, back off for the indicated duration instead of retrying immediately.
1,340 words · updated Aug 25, 2026