Skip to content

Realtime limits and errors

Every limit you can hit and every error you can be handed, in one place.

Every client-visible limit, HTTP status code, WebSocket close code and error frame. The five routes a public key reaches are on the public HTTP routes; the management routes — namespaces, grants, webhooks and the archive — have their own Realtime API reference.

Names and identifiers

ThingLimit
Channel namenamespace:rest, 3–128 characters of [a-zA-Z0-9._:-]. A name with no namespace segment is invalid.
Namespace name1–64 characters of [a-zA-Z0-9._-]. No colon.
Token subject1–128 characters of [a-zA-Z0-9._-].
Grant patternA concrete namespace segment, then up to 7 more segments; a segment may be *, which matches exactly one segment.

Tokens

ThingLimit
Channels per token1–32
Operations per channel1–4, from subscribe, publish, presence, history
Token lifetime60–3600 seconds; 3600 when you omit it

Request and message size

ThingLimit
Request body, all routes except POST /messages64 KiB (65 536 bytes)
Request body, POST /messages256 KiB (262 144 bytes)
Client event published from a browser32 KiB (32 768 bytes) of serialized data

POST /messages has the larger limit because payloads are inlined up to 256 KiB. Within a publish, the serialized data is compared against that same figure. At or below it, the payload is delivered inline. Above it, the payload is stored and subscribers receive a reference instead: a ref object with bucket_key, size and, when known, content_type. History entries follow the same rule — an entry carries either data or ref, never both.

Store-and-link needs payload storage provisioned for the product. Without it, an oversized publish fails with 503; publishes at or under the inline cap are unaffected.

Publish quota

Publishes are counted per product over fixed 10-second windows.

ValueDefault
Sustained rate50 publishes per second
Window10 seconds
Burst allowance100 publishes per window
Hard limit1 100 publishes per window (burst + twice the sustained allowance)

These are the defaults a product starts with; they can be tuned per deployment, so measure rather than assume if the numbers matter to your design.

Only crossing the hard limit is rejected. There is no cliff at the sustained rate: you may run over it for a while, which is what the burst and the doubled allowance are for. A rejected publish returns 429 with a Retry-After header giving the seconds until the current window rolls.

The quota is accounted separately from the edge’s request rate limit of 100 requests per minute per IP address, and both apply.

Client events

Publishing from a browser is off unless the namespace enables client events. When it is on, each connection gets its own token bucket: 5 publishes per second, with a burst of 10. Over that, the publish is answered with an error frame of code rate_limited — the connection stays open.

Client events are ephemeral. They fan out and are never stored, so they carry no offset or epoch and cannot be replayed. The publishing connection is excluded from delivery, so the acknowledgement is its only confirmation.

Presence

ThingLimit
Members per channel100
Member info10 KiB (10 240 bytes) serialized
Entry lifetime1–3600 seconds; 60 when not given

The roster is deduplicated by identity, so one person on several tabs is one member with a connections count. Entering a full roster is refused with 429.

History

ThingLimit
last_n1–1000 entries
last_msAny positive number of milliseconds
Retention windowUp to 30 days, set per namespace
Archive retentionUp to 3650 days, set per namespace

History exists only where the namespace retains it. A read on a namespace with history off returns an empty result rather than an error.

A cursor is offset plus epoch together. Ask for a cursor or a window, never both and never neither. When the answer comes back with recovered: false, the cursor’s epoch no longer matches or the entries it pointed at have aged out: the client missed messages and must resync, not continue. See history.

HTTP status codes

CodeWhat it meansWhat to do
400The request failed validation — a malformed channel name, a half cursor, a cursor and a window together, a missing required field. The body names the failing fields.Fix the request. Retrying it unchanged will fail the same way.
401public_key_required: no public key was presented.Send the key as Authorization: Bearer <key>.
401public_key_invalid: the key matches no active key of the product in the path — unknown, mistyped, revoked (once the revocation has propagated), belonging to another product, or created on the other environment.Unknown, revoked and foreign are deliberately not told apart, and the response does not name the environment that answered. Check the edge origin before you suspect the key, and give a fresh key its propagation window.
403public_key_scope: the key is active but its scope does not cover the route you called, or it was revoked within the last propagation window.Widen the key with Edit scope on Product → Settings → Public Access, or use a key that already covers Realtime. Retrying unchanged will not help.
403Presence is not enabled for the channel’s namespace.Enable presence on the namespace, or stop reading presence there.
404The namespace is not registered for your product.Register the namespace.
413The request body is over the cap for the route.Publish a smaller payload; see message size.
422Every capability you declared on POST /tokens/issue was stripped by namespace policy, leaving nothing to mint.Check the namespaces are registered and their policy allows the operations you asked for.
429A publish beyond the hard quota, or presence on a full roster. Publishes carry Retry-After.Wait out Retry-After and retry. If it recurs at a steady rate, you are over the sustained quota, not merely bursting.
503public_config_unavailable: no key snapshot is loaded yet and the edge fails closed.Retry with backoff. Your key is not the problem.
503Fan-out or history is unavailable, or a payload is over the inline cap and no payload storage is provisioned.Retry with backoff. The oversized-payload case will not clear on its own — provision storage or publish smaller payloads.

The three public_key_* outcomes and public_config_unavailable come from the platform edge, before the request reaches Realtime. They answer {"error": {"code": …, "message": …}}, and the three key outcomes carry "step": "public_key_check" beside it so they can be filtered together in a log. Key changes reach that edge through a snapshot refreshed roughly every 30 seconds. See authentication.

WebSocket close codes

The browser client handles these for you; they matter when you are reading logs or writing your own client.

CodeWhat it meansWhat the client does
4001The connection carries no valid token.Reconnect immediately with a freshly minted token.
4100The realtime service is draining, on shutdown or redeploy.Reconnect immediately; another node takes the connection.
4200Slow consumer: the connection’s per-channel buffer overflowed and messages were dropped.Reconnect with backoff and come back through cursor recovery — the gap is real and must be replayed, not ignored.

@lessly/realtime-client reconnects at once on 4001 and 4100, since neither means the connection was at fault, and backs off on everything else.

WebSocket error frames

An error frame does not close the connection. It answers one action.

CodeWhat it means
invalidThe frame was malformed, or the action was not allowed in this state — an unparseable frame, a missing channel, a client publish over 32 KiB, presence info too large, or a presence update from a connection that is not in the roster.
forbiddenThe token does not carry the operation for that channel.
unavailableThe realtime service is up and can verify your token, but cannot serve subscribe, publish or presence work right now. Retry with backoff.
rate_limitedThe connection exceeded its client-event budget.

forbidden will not fix itself by retrying: the token is the problem, so mint a new one with the operation you need — if namespace policy allows it.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

navigateselect