Skip to content

Realtime

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 methods your API key reaches are listed on the server SDK page; the platform’s management routes — namespaces, grants, keys, webhooks and the archive — have their own Realtime API reference.

The public routes

Your backend reaches Realtime at one base path:

https://public.lessly.com/{productId}/realtime

{productId} is the id of your product on the platform, and every route is relative to that base — so a publish is POST https://public.lessly.com/{productId}/realtime/messages. The five methods an API key reaches are listed on the server SDK page.

Authentication

Send your product API key in the X-Api-Key header. A key is rtk_ followed by 43 base64url characters of randomness — nothing else, and nothing in it encodes an environment or a product.

X-Api-Key: rtk_…

The key is resolved to a product, and that product must be the one in the path.

CaseAnswer
Key missing, malformed, unknown or revoked401 with the message invalid_api_key, plus a hint and the gatewayUrl of the environment you actually reached
Valid key, but for a different product than the path404, not 403 — so the surface cannot be used to probe for other products

Keys are per environment. A key minted on one environment does not exist on another, and reads there as simply invalid; the gatewayUrl in the 401 tells you which environment you hit.

This key is a backend credential. Never ship it to a browser — browsers get a capability token from POST /tokens/issue instead. See authentication.

CORS

Browser requests are accepted only from https://*.lessly.dev. Your own application origins are not on that list, which is deliberate: these routes are for your backend, and the key they need must never reach a browser.

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 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.
401invalid_api_key: the X-Api-Key header is absent, malformed, unknown or revoked. The body carries a hint and the gatewayUrl of the environment you reached.Check you are sending a key minted for that environment. Unknown and revoked are deliberately not told apart.
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 — or your key is valid but belongs to a different product than the one in the path.Register the namespace. If the namespace is right, check the product id in the URL against the key you used.
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.
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.

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