Search the complete documentation

Try idempotency, proposal lifecycle, authentication, or ordering.

Navigate Open
繁體中文
Browse documentation

Relay · Limits

Limits, errors, and backpressure

Relay rejects excess work before executing the requested conversation action. Clients should wait for the returned retry window, preserve message nonces across uncertain sends, and treat authorization or validation failures as state changes rather than transport failures.

Current default budgets

These are the current self-hosted defaults. A deployment operator can replace them per tenant.

BudgetScopeDefault
Message writesUser within tenant10 per second
Message writesEntire tenant100 per second
WebSocket framesUser within channel30 per second
WebSocket joinsUser, separate join scope30 per second
Attachment presignUser within tenant30 per minute
Attachment presignEntire tenant600 per minute
Attachment download URLUser within tenant120 per minute
Search queriesUser within tenant60 per minute
Search queriesEntire tenant600 per minute
Host credential syncTenant20 per second
Host credential syncSource IP60 per second
Outbound host webhooksTenant and destination URL50 per second

Message write budgets apply to REST message create, edit, retract, pin, unpin, and reaction mutations. WebSocket message send, edit, retract, and reaction events use the same user and tenant budgets, with the additional per-channel frame budget. Pin and unpin events use the frame budget only.

Channel creation, membership changes, channel close, metadata updates, notification mute, and ordinary reads do not currently share a general request budget. Search and attachment operations have their own limits.

REST rate-limit response

A limited message, search, upload, or download request returns 429 Too Many Requests:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{
  "error": "rate_limited"
}

Retry-After is a whole number of seconds rounded up from the remaining window. Wait at least that long before retrying.

Host credential sync also returns 429, but it currently uses an errors array and does not include Retry-After. This is a response consistency gap, not a separate retry guarantee.

WebSocket rate-limit response

An in-channel event returns its delay in milliseconds:

{
  "error": "rate_limited",
  "retry_after_ms": 847
}

This shape applies to message writes and frame-limited events such as cursor updates, pins, thread reads, and typing indicators.

A rate-limited channel join returns only:

{
  "reason": "rate_limited"
}

The join response has no retry value. Back off with jitter before joining again. Do not reconnect in a tight loop.

Common HTTP status codes

StatusMeaningClient action
400Malformed admin or operator requestCorrect the request shape
401Missing, expired, revoked, or invalid credentialsRefresh or replace credentials
403The actor lacks membership or permissionRevalidate host authorization before retrying
404The resource is absent or hidden by tenant and resource scopeDo not assume another tenant contains it
409The resource lifecycle conflicts with the operationRefresh current state before deciding what to do
410An attachment is tombstonedRemove the stale attachment reference
422Domain validation failedCorrect the payload or product state
429A rate budget was exhaustedWait for the retry window
500Relay could not complete the requestRetry only when the operation has an idempotency strategy

Most conversation endpoints return errors as an array:

{
  "errors": ["forbidden"]
}

Rate-limit, search, and attachment controllers use a singular error field. Clients must currently support both envelopes. A unified error code and response schema are not implemented.

Retry by operation

For message creation, keep the original nonce after a timeout, disconnect, 429, or retryable 5xx. If the first attempt committed, Relay returns the existing message instead of creating another one.

For reads, retry after the provided window. For edits, reactions, membership changes, channel lifecycle actions, and attachment operations, first determine whether the previous attempt committed. These operations do not all expose a client idempotency key.

Do not retry 401, 403, 404, 409, or 422 unchanged. A credential, permission, resource, lifecycle, or payload condition must change first.

Operator overrides

An operator can replace a tenant’s stored overrides through the admin API:

PUT /api/admin/tenants/{tenant_id}/rate_limits
X-Admin-Token: <admin-token>
Content-Type: application/json

{
  "rate_limits": {
    "user_message": {
      "scale_ms": 1000,
      "limit": 20
    },
    "tenant_message": {
      "scale_ms": 1000,
      "limit": 200
    }
  }
}

The request replaces the complete override map. Omitted budgets fall back to deployment defaults. Sending an empty map clears every override. Values must contain positive integer scale_ms and limit fields.

This endpoint requires the deployment admin token. It is not a tenant self-service control plane.

Backpressure boundary

Rate-limited synchronous REST and WebSocket operations are rejected before the conversation action runs. Relay does not queue those client requests for later execution.

Post-commit WebSocket fan-out, host webhook delivery, search indexing, and adapter egress use durable internal handoffs. A host webhook that exhausts its outbound budget is delayed rather than dropped immediately. This protects request latency, but it does not promise a maximum delivery delay.

Message order still comes from channel-local seq, not worker completion or webhook arrival order.

Product boundary

The current limiter uses node-local ETS counters. In a multi-node deployment, each Relay node enforces its own counter, so the table above is not a cluster-wide aggregate ceiling.

Cluster-wide rate accounting, complete coverage for channel and membership operations, consistent error envelopes, standard limit and remaining headers, and a retry value on WebSocket join remain product work.

Treat the defaults as current deployment behavior, not a managed-service quota or SLA.