Rate limits and flow control
The short-window limits each segment runs under, how the SDK slows down, pauses and retries for you, and what each error means.
Rate limits keep a node predictable and stop one runaway client from slowing everyone else down. They are guardrails, not billing: they count per second (with a short burst allowed) or per minute, never per hour or day. Monthly message and data allowances are a separate thing, set by your plan.
A segment's limits come from its plan: they grow with the package size (concurrent connections) and follow the segment's preset, which shapes them for what the segment is used for. Each node counts on its own, so in a cluster the limits are approximate.
What the SDK does for you
Most apps need no code for any of this:
| What happens | What the SDK does | What you see |
|---|---|---|
| A publish goes over a rate | Waits as long as the node asks, then sends it again with the same idempotency key, until publishTimeoutMs | Nothing, unless it is still refused after 30 seconds (Timeout) |
| The segment's deliveries fall behind | Sends publishes in batches (throttled), or holds them until the node resumes (paused) | client.connection.flow |
| A subscribe or a new topic goes over its per-minute rate | Tries again after the wait the node asks for | sub.ready or the publish settles a little later |
| Too many new connections at once (the segment's per-minute limit, or too many failed sign-ins from one address) | Reconnects after the node's wait, plus some random extra so a crowd of clients doesn't come back together | The state stays connecting or reconnecting |
| A topic already has as many subscribers as the plan allows | Nothing: retrying can't help | sub.ready rejects with QuotaExceeded |
| A payload is over 60 KiB | Refuses it before sending | publish() rejects with PayloadTooLarge |
None of these close the connection.
Flow control
When a segment's deliveries go over its outbound byte rate, the node never drops events. It slows the publishers down instead, and the SDK follows:
connection.flow | Means | What happens to your publishes |
|---|---|---|
'normal' | All is well | Sent as you make them |
'throttled' | Deliveries are falling behind | Still accepted; the SDK sends them in batches, up to once a second |
'paused' | Deliveries are more than a second behind | Held in the queue, in order, and sent when the node resumes. Signals are dropped, as when offline |
While paused, a publish still waits at most publishTimeoutMs (30 seconds) in total before failing with Timeout,
and at most offlineQueue.maxSize publishes (1,000) wait; more fail at once with QueueFull. Nothing is dropped
without its promise saying so.
Show it in your UI, for example to grey out a send button:
import { useSyncExternalStore } from 'react';
function SendButton({ client }: { client: Realtime }) {
const { flow } = useSyncExternalStore(client.connection.subscribe, client.connection.getSnapshot);
return <button disabled={flow === 'paused'}>{flow === 'throttled' ? 'Send (busy)' : 'Send'}</button>;
}Or listen for changes:
client.connection.on('flow', ({ state, retryAfterMs }) => {
console.log(`publishing is ${state}`, retryAfterMs); // how far behind the node's deliveries are
});See your limits
session.limits shows the limits the node announced for this connection:
const session = await client.connect();
session.limits.publishPerSec; // e.g. 20
session.limits.maxSubscribersPerTopic; // e.g. 5000| Limit | Scope | Over it |
|---|---|---|
publishPerSec, publishBurst | Publishes from this connection, per second, and the burst above it | RateLimited |
topicPublishPerSec, topicPublishBurst | Publishes into one topic from all publishers | RateLimited |
segmentPublishPerSec, segmentPublishBurst | Publishes across the whole segment | RateLimited |
inboundBytesPerSec | Bytes all the segment's clients send | RateLimited on publishes and subscribes |
outboundBytesPerSec | Bytes the segment's subscribers receive | Flow control |
maxSubscribersPerTopic | Subscribers one topic holds at once | QuotaExceeded on subscribe |
subscribesPerMin | Subscribes across the segment | RateLimited |
topicCreatesPerMin | New topics across the segment | RateLimited |
newConnectionsPerMin | New connections across the segment | A connection attempt is turned away, and the SDK retries |
A limit is missing when the node doesn't announce it: an older node, or a connection over HTTP long-polling, which has no handshake. The SDK then relies on the node's answers alone, as it always does.
Limits by preset
Each preset gives the limits a different shape. The numbers depend on your plan and package size, and may change,
so read them from session.limits rather than hard-coding them.
| Preset | Built for | Shape |
|---|---|---|
| General | Segments that chose no preset | Balanced |
| AI streaming | Token streams from a model | A high publish rate into a few topics, high inbound bytes, fewer subscribers per topic |
| Chat | Rooms and conversations | Many topics, each with a low publish rate; more topic creations and subscribes per minute |
| Live dashboard | Many viewers of the same data | Very many subscribers per topic and high outbound bytes |
| IoT | Many small devices | A low rate per connection, a high rate across the segment |
Segment-wide limits grow with the package, never below a floor that keeps small packages working. The payload cap
is the same on every package: 60 KiB per event (limits.maxPayloadBytes, 61,440 bytes).
What each error means
| Error | Means | What to do |
|---|---|---|
RateLimited (300) | Over one of the rates above, or publishing is paused. Nothing was done | The SDK retries after retryAfterMs. If publishes still time out, publish less often |
QuotaExceeded (301) | A count is full: subscribers on this topic, topics in the segment, connections, or the month's messages | Retrying soon won't help. Unsubscribe elsewhere, raise the limit in Settings, or upgrade the plan |
PayloadTooLarge (302) | The payload is over 60 KiB | Send less, or a reference to the data |
Backpressure (303) | This connection read too slowly and was closed | Nothing: the SDK reconnects and resumes without losing events |
Staying under the limits
- Batch high-frequency updates. A cursor or a progress bar doesn't need every intermediate value: send the latest every 100 ms or so.
- Use the right delivery type. Latest-value state belongs in kv, and "who is typing" in signals, which are cheap and dropped when stale.
- Stream AI output in sensible chunks. One event per few tokens is plenty; 60 KiB holds about 10,000 tokens.
- Reuse topics. Creating topics on every page view counts against
topicCreatesPerMin. - Don't reconnect on your own. The SDK already spreads reconnects out. Opening new clients in a loop is what the connection limits are there to stop.