Limits and errors

This page lists every limit the relay enforces and every error it returns. Use it to size an integration, to understand a 429 or 503, and to look up an error code. The routes themselves are documented on the pages linked from Relay API; this page does not repeat their tables.

How limits work#

The limits protect each Mac from the others, and the relay from running past Cloudflare's free plan. Every paired Mac has its own mailbox, so a per-device counter is never shared: one noisy Mac or key cannot use up another Mac's allowance.

  • Counters that reset daily reset at midnight UTC.
  • A relay's owner can change most limits with a Worker variable, in wrangler.toml under [vars] or with --var NAME:value. A variable counts only when it is a positive number. Otherwise the default applies.
  • QUEUE_TTL_HOURS is in hours. Every other variable is a plain count or a number of bytes.
  • The defaults for the per-device limits are in relay/src/limits.ts. Other limits are constants named in the Source column.

Limits#

Per Mac#

LimitDefaultWorker variableWhen it is reachedSource
Notifications per day500DEVICE_NOTIFICATIONS_PER_DAY429 daily_cap on POST /v1/notify.limits.ts
Undelivered notifications waiting100DEVICE_QUEUE_MAX429 queue_full on POST /v1/notify.limits.ts
Wait for an undelivered notification24 hoursQUEUE_TTL_HOURSThe notification gets a suppressed receipt with reason expired.limits.ts
Agent requests per day5000DEVICE_REQUESTS_PER_DAY503 budget_exhausted on agent routes.limits.ts
Voice replies per day40DEVICE_AUDIO_UPLOADS_PER_DAY429 daily_cap on the audio upload.limits.ts
Voice reply bytes per day20 MBDEVICE_AUDIO_BYTES_PER_DAY429 daily_cap on the audio upload.limits.ts
Long-poll seconds per day3000DEVICE_POLL_SECONDS_PER_DAYA wait is treated as 0, so the relay answers at once.limits.ts
Notify request size32 KBMAX_BODY_BYTES413 too_large. A data: icon may add up to 256 KB.limits.ts
Active agent keys20none429 too_many_keys when a key is made or a connector approved.mailbox.ts

Notes on the table:

  • The request budget counts agent requests, device requests (except the stream and usage) and MCP calls. Only agent routes are refused once it is used up. The Mac can still connect, collect its queue and send receipts. A notification already queued is not lost.
  • A 503 budget_exhausted and a 429 daily_cap carry retryAfterSeconds and a Retry-After header counted to the next midnight UTC.
  • The daily request budget is kept below Cloudflare's free-plan allowance of 100,000 requests a day, so Cloudflare never has to cut the Mac off mid-day. GET /v1/device/usage reports every counter and its limit. See Pairing and devices.

Per key#

LimitDefaultWorker variableWhen it is reachedSource
Notifications per 10 minutes60RATE_LIMIT_PER_KEY429 rate_limited with Retry-After.limits.ts
Receipt, reply and events calls per 10 minutes600none429 rate_limited, retry in 60 seconds.mailbox.ts
Long-poll wait60 secondsnoneA larger wait is cut to 60.mailbox.ts
Event subscriptions per connector5none-32602 on events/subscribe.events.ts

The 600 per 10 minutes applies to receipt reads, reply reads, events/subscribe and events/unsubscribe. The status check and notify are outside it. The count is held in memory, so a relay restart forgives the current window. The request budget still holds.

Pairing, approvals and registration#

LimitDefaultWorker variableWhen it is reachedSource
Macs that may pair5MAX_DEVICES403 device_limit on POST /v1/pair/start.registry.ts
Pairing starts per address per hour6PAIR_STARTS_PER_HOUR429 rate_limited.registry.ts
Pairing starts per hour, all addresses300PAIR_STARTS_GLOBAL_PER_HOUR429 rate_limited.registry.ts
Pairing codes waiting3 per address, 60 in allnone429 rate_limited on POST /v1/pair/start.registry.ts
Wrong pairing codes per address per 10 minutes10none429 rate_limited on POST /v1/pair.registry.ts
Client registrations per hour30REGISTER_PER_HOUR429 rate_limited on POST /register.registry.ts
Connector approvals waiting per Mac3none429 rate_limited, or slow_down on the device flow.mailbox.ts
Connector approvals begun per hour12none429 rate_limited, or slow_down on the device flow.mailbox.ts
Wrong approval codes per request5noneThe request is denied.mailbox.ts

The client address is hashed before it is compared and is never stored in the clear. Pairing again under the same device name does not count against the limit of paired Macs.

Field and size limits#

The notification fields have their own maximum sizes, listed with each field under POST /v1/notify. These are the values the relay enforces in relay/src/validate.ts.

FieldLimit
title, subtitle200 characters.
body8000 characters.
project, session, task, tool, group100 characters each.
status32 characters.
duration40 characters.
notificationId128 characters.
link, imageURL, icon as a URL2048 characters.
icon as a data: image256 KB.
tags10 tags of at most 32 characters.
speak text2000 characters.
timeoutSeconds1 to 3600.
speed, speak.speed0.5 to 2.

Other sizes:

ValueLimit
A voice reply upload1 MB, audio/mp4, audio/x-m4a or audio/aac.
A key name1 to 32 characters: a-z, 0-9 and -.
A device name at pairingThe first 60 characters.
A registered client nameThe first 60 characters.
Redirect URIs per registered client1 to 10.
Callback URL for an event subscription2048 characters.

Fixed lifetimes and intervals#

ValueFixed at
Pairing code lifetime10 minutes, single use.
Browser approval request lifetime10 minutes.
Device approval request lifetime10 minutes.
Time to exchange an approved code5 minutes.
Device flow poll interval5 seconds, raised by 5 for each slow_down.
Signed audio link3600 seconds.
Stored voice reply7 days, by a rule on the audio bucket.
Notification ids, receipts and repliesKept 24 hours.
Event delivery timeout10 seconds.
Event delivery attempts8, within 24 hours.
Event history for replay30 days, at most 100 events per replay.
Mac counted as onlineSeen within 11 minutes.
Idle Mac removed by the relay30 days without a connection.
Idle Mac another Mac may remove7 days without a connection.
Access tokens kept per connector50 of the newest.

Access tokens and refresh tokens never expire. They stop working only when the connector is revoked. A refresh returns a new access token and the same refresh token, and expires_in is reported as ten years.

Access settings#

Two secrets and one list control access rather than limits.

SettingPurpose
Secret RELAY_SECRETRequired. It signs device ids so strangers cannot create mailboxes. Without it the relay answers 500 misconfigured.
Secret PAIRING_SECRETOptional. When set, POST /v1/pair/start needs it in X-Pairing-Secret.
Variable EVENT_CALLBACK_HOSTSOptional, comma-separated. Limits the hosts an event subscription may call. Empty means any public host.

Errors#

Error body#

Every error from a relay route has a JSON body with an error code and a message. One more field is added where it helps.

JSON
{
  "error": "rate_limited",
  "message": "at most 60 notifications per 10 minutes per key",
  "retryAfterSeconds": 412
}
FieldTypeDescription
errorstringA stable code from the table below. Match on this, not on message.
messagestringA sentence for a person. The wording can change.
fieldsarrayThe rejected field names, on invalid_request and forbidden_fields.
retryAfterSecondsintegerHow long to wait, on a rate limit or an exhausted budget.
statusstringThe decision already made, on already_decided.

A Retry-After header carries the same number of seconds for these errors: rate_limited and daily_cap on POST /v1/notify, rate_limited on the read limit, daily_cap on a voice reply upload, and budget_exhausted. The pairing, registration and approval-begin limits send retryAfterSeconds in the body and no header.

Two families use another format:

  • The OAuth routes answer in the OAuth error format, error and error_description. See OAuth errors.
  • MCP errors are JSON-RPC errors. See JSON-RPC errors.

Error codes#

CodeStatusMeaning
invalid_request400The body is not valid JSON, or a value is invalid. fields names the field.
forbidden_fields400The body holds a field the relay does not accept. fields lists it.
scope_not_allowed400A key was asked for with a scope other than notify.
unauthorized401The credential is missing, malformed, revoked or unknown, or a pairing secret is required.
wrong_credential403An agent key or access token was used on a device route, or the device token on an agent route.
scope403The key has no notify scope.
forbidden403A signed audio link has a bad signature.
device_limit403The relay already has its maximum number of paired Macs.
bad_code403A pairing code is wrong, used or expired.
not_removable403One Mac tried to remove another Mac that is active, has another name and has valid credentials.
not_found404The route, notification, key, device or entry does not exist. Notification ids are kept 24 hours.
not_paired404A connector approval was started on a relay with no paired Herald.
method_not_allowed405An OAuth route was called with the wrong method.
name_taken409An active key already has that name.
already_decided409A connector request was already approved, denied or expired.
expired410A connector request or a signed audio link has expired.
too_large413A request or an audio upload is over its size limit.
unsupported_media_type415An audio upload is not audio/mp4, audio/x-m4a or audio/aac.
upgrade_required426The stream route was called without a WebSocket upgrade.
rate_limited429A per-key, per-address or approval rate limit was reached. See retryAfterSeconds.
daily_cap429The Mac's daily notification, voice reply count or voice reply byte allowance is used up.
queue_full429The Mac has too many undelivered notifications waiting. It has been offline too long.
too_many_keys429The Mac already has 20 active keys or approvals.
internal500The relay failed unexpectedly.
misconfigured500The relay has no RELAY_SECRET.
budget_exhausted503The Mac's daily request budget on the relay is used up. Nothing queued is lost.

OAuth errors#

The OAuth routes answer with {"error": "...", "error_description": "..."}. Token errors are status 400, except where the table says otherwise. The routes that return them are documented in OAuth and Device flow.

CodeStatusWhen
invalid_client_metadata400A registration field is not valid JSON or has an unsupported value.
invalid_redirect_uri400redirect_uris is not 1 to 10 https or loopback http URLs without a fragment.
invalid_client401The client id is unknown or its authentication failed. A Basic attempt adds WWW-Authenticate.
unsupported_grant_type400grant_type is not one of the three grants the relay supports.
invalid_grant400A code, refresh token or device code is unknown, used, malformed, mismatched or revoked.
invalid_scope400A scope other than notify was asked for.
invalid_target400resource does not match the original request.
invalid_request400The Mac is not paired, or device is out of range.
authorization_pending400The user has not decided yet. Poll again after the interval.
slow_down400A device poll came too soon. The interval grows by 5 seconds.
slow_down429Approvals are already waiting, or too many were started. Retry-After is 60.
expired_token400The device code or its approval has expired. Start again.
access_denied400The user denied the request.
temporarily_unavailable502The relay could not start the approval.

JSON-RPC errors#

MCP calls report errors as JSON-RPC error objects in the body. The HTTP status is 200 unless the table says otherwise. The methods that use them are on POST /mcp and in Relay events.

CodeStatusWhen
-32700400The body is not valid JSON.
-32600400 or 202A batch was sent, which is not supported (400), or the body is a response, not a request (202).
-32601404 or 200The method does not exist. A client on an older protocol sees 200.
-32602400 or 200A parameter is invalid, or the protocol _meta is incomplete (400).
-32603200, 429 or 503The relay could not handle a subscription right now.
-32000405GET or DELETE on /mcp: the server answers POST only.
-32015200An event callback failed verification. error.data.reason says why.
-32020400The Mcp-Method or Mcp-Name header does not match the body.
-32022400The protocol version is not supported. error.data lists supported and requested.

A missing or invalid connector credential on /mcp is HTTP 401 with a WWW-Authenticate header that points to the discovery document, so an OAuth client knows where to start. It is not a JSON-RPC error.

Edit this page on GitHub

Esc
Getting started
Guides