Limits & quotas
poolse has three kinds of limit. Each fails in its own way, so your code can tell them apart.
| Kind | Depends on plan? | When you hit it | What to do |
|---|---|---|---|
| Plan quota | Yes | 402 with a specific error code | Show an upgrade prompt, or reduce usage. Retrying won't help. |
| Abuse limit | No, same for everyone | 429 (or a dropped event, see below) | Back off and retry later. |
| Fixed limit | No | 422 or 400 | Fix the request. |
Plan quotas
| Quota | Free | Pro | Scale | Error when exceeded |
|---|---|---|---|---|
| Monthly active users | 1,000 | 10,000 | 50,000 | 402 mau_cap_exceeded |
| End-users (how many exist) | Unlimited | Unlimited | Unlimited | — |
| Webhooks | 5 | Unlimited | Unlimited | 402 webhook_count_cap_exceeded |
| Webhook deliveries, rolling 24 hours | 1,000 | Unlimited | Unlimited | None. Extra events are not delivered (see below). |
| Hosted attachment storage, total | 300 MB | 500 MB | 500 MB | 402 attachment_total_cap_exceeded |
| Size of one attachment, hosted storage | 1 MB | 25 MB | 25 MB | 402 attachment_too_large |
| Your own bucket (BYO storage) | Included | Included | Included | — |
| Voice call minutes, per calendar month | 100 | 10,000 | 100,000 | 402 call_minutes_exhausted |
| Video call minutes, per calendar month | 50 | 2,000 | 25,000 | 402 call_minutes_exhausted |
| Messages | Unlimited | Unlimited | Unlimited | — |
| Conversations | Unlimited | Unlimited | Unlimited | — |
Paid plans are activated by the poolse team for now; there is no self-serve checkout yet. A workspace on a trial gets the Free quotas, except 50 monthly active users.
MB here means MiB (1 MB = 1,048,576 bytes). Storage counts attachments that have been sent in a message; pending uploads don't count.
The two attachment quotas apply to poolse's hosted storage. With your own S3-compatible bucket connected (Bring your own storage), new uploads go to your bucket and skip both: poolse sets no storage limit there, and a file can be up to the 100 MB fixed limit on any plan. Attachments stored before you connected it stay in hosted storage and still count.
Where each quota is checked
mau_cap_exceeded: on any request made with an end-user JWT, and on opening the socket, by a user not yet active this month once the month is full. See below.details: { count, cap }.webhook_count_cap_exceeded: creating a webhook, in the API or the dashboard.details: { count, cap }.attachment_too_large: requesting an upload URL for a file over the per-file cap, while attachments go to hosted storage.details: { bytes, cap }.attachment_total_cap_exceeded: requesting an upload URL that would take the workspace over its hosted storage.details: { used, would_use, cap }.
Attachment quotas are checked against the byte_size you declare when
you request the upload URL. The upload URL itself does not enforce the
size or the content type, so poolse checks the stored file when the
attachment is sent in a message: if it is bigger than declared, the send
is refused with 422 send_failed.
Monthly active users
A user is active in a calendar month (UTC) from the first time they use your app that month: a request with their end-user JWT, a socket connection, answering or acting on a call with a call grant (a phone answering with the app closed), or replying to or marking read from a notification. Your backend's API-key calls (creating users, minting tokens) don't make anyone active, so you can create users ahead of time freely. Being rung or sent a notification doesn't either.
When the month already has the plan's number of active users, a user who
has not been active yet this month is refused: REST requests, call grant
and notification actions return 402 mau_cap_exceeded, and the socket
does not connect. Users already
active this month keep working, so the people using your app now are
never cut off. The count starts again on the 1st. Two users arriving at
the same moment at the cap can both get in, so the count can end one or
two over.
The live count is on Overview and Billing.
Call minutes
Calls are counted in participant-minutes: each person's time in a call or room, added up. A 10-minute call between two people is 20 minutes; three people for 10 minutes is 30. The month's total per medium is rounded up to a whole minute, and resets at 00:00 UTC on the 1st.
What counts, exactly:
- Ringing is free. A call's caller is counted from the moment someone else is in the call, not from placing it. A call nobody answers costs nothing.
- Rooms count from arrival, alone or not: being in a room is using it.
- Voice and video are counted apart. A video call counts as video from the start. A voice call or room becomes video for everyone in it the moment anyone turns a camera on, and stays video until it ends; people joining after that count as video too.
Once a medium's minutes are used up:
- Placing a call, starting or joining a room, joining a live call,
ringing more people into a call ("Add people") and fetching a media
token for a call of that medium return
402 call_minutes_exhaustedwithdetails: { media, used, cap }(mediaisaudioorvideo). What counts is the medium of the call you would end up in: asking for a voice room while a video call is live checks video. - Calls already connected carry on until they end, and answering a call that is already ringing still works.
- The other medium is not affected: with video minutes used up, voice calls work while voice minutes last, and their media tokens can't publish a camera, so a voice call can't turn into video. Your app can offer a voice call instead.
The live count is on the dashboard's Calls and Billing pages.
The webhook delivery quota
When the workspace has made 1,000 deliveries in the last 24 hours, new events are not delivered and not queued. Nothing returns an error, because the action that triggered the event (a message send, say) succeeded. Deliveries start again once the 24-hour count drops below the cap. The count is refreshed about once a minute, so a burst can go a little over 1,000. Retries of an existing delivery don't add to the count. Each webhook that receives an event counts as one delivery.
You can watch the count on Billing.
Inactive subscriptions
If your organisation's status is canceled or suspended:
- Sending a message returns
403 forbidden. - Requesting an upload URL and creating a webhook return
402 subscription_inactive.
Reads, edits and deletes keep working. See Billing.
Abuse limits
These protect the service and are the same on every plan.
| What | Limit | Per | What happens |
|---|---|---|---|
Minting user tokens (POST /v1/users/:user_id/tokens) | 60 per minute | API key | 429 rate_limited with a Retry-After header (seconds) |
Lazily created users (unknown external_ids in a new conversation or added members) | 50 per hour | Calling user | 429 lazy_provision_rate_limited with a Retry-After header and details.retry_after_ms |
| Typing events on the WebSocket | 10 per second | User and conversation | The typing push is answered with an error reply { reason: "rate_limited", retry_after_ms } |
| Webhook events dispatched | 1,000 per minute | Workspace | Events over the limit are not delivered; no error |
The SDK retries 429 responses on its own, waiting for the longer of the
Retry-After value and its own backoff, and throws RateLimitedError
when it runs out of retries.
Message sends and reads have no rate limit of their own.
Fixed limits
| What | Limit | Error |
|---|---|---|
Message body | 64 KiB (65,536 bytes), and not blank unless the message has attachments | 422 validation_failed |
| One attachment | 100 MB | 422 byte_size_too_large |
| Upload URL lifetime | 15 minutes | — |
| Download URL lifetime | 1 hour | — |
| User token lifetime | 1 hour | 401 once expired |
external_id | 1–255 characters | 422 validation_failed |
display_name | up to 255 characters | 422 validation_failed |
Conversation name | up to 255 characters | 422 validation_failed |
avatar_url (users and conversations) | up to 2,048 characters | 422 validation_failed |
| Ids | UUIDs | 404 for a non-UUID id in the path; 400 bad_request for one in the body or query string |
| People in one call or room | 2–16, set per workspace (default 16) | The next person is refused |
| Ring time | 15–120 seconds, set per workspace (default 45) | — |
Call limits are set on the dashboard's Calls page.
Handling these in code
import { ApiError, RateLimitedError } from '@poolse/sdk';
try {
await sendSomething();
} catch (err) {
if (err instanceof RateLimitedError) {
// Abuse limit: the SDK already retried. Try again later.
} else if (err instanceof ApiError && err.status === 402) {
// Plan quota: err.code is e.g. 'mau_cap_exceeded', err.details has the numbers.
showUpgradePrompt(err.code, err.details);
} else {
throw err;
}
}
Every error code is described on Errors.