src/docs/src/rate-limits-and-quotas.md
Three separate mechanisms decide whether a call succeeds. They are independent, and hitting any one of them is enough to stop a request:
| Mechanism | Bounds | Refills | Failure |
|---|---|---|---|
| Usage credit | what usage costs (AI, egress, KV capacity, storage ops, workers) | monthly, per plan | 402 insufficient_funds |
| Rate limit | how many requests are made per window | rolling window (10s / 1min / 1h) | 429 too_many_requests |
| Storage quota | how many bytes are kept in the filesystem | never — the user deletes or upgrades | 413 storage_limit_reached |
A credit balance does not buy rate-limit headroom, and an empty balance does not stop metadata reads that cost nothing. Design for all three.
Because of the User-Pays Model, every limit below applies per user, not per app: your app's traffic is bounded by each of your users' own accounts, so one heavy user can never exhaust your app for everyone else.
Usage is charged against the account's monthly credit allowance, metered per operation at real cost.
What usage costs (the big three):
puter.ai.listModels() reports models; the per-model rates are served by the API (GET /metering/allCosts) rather than printed here, because a single number would be wrong for every model.A streamed AI response that stops before the model reports its token counts — an upstream error part-way through the response, say — is still charged, on an estimate of what it streamed. A request that produced no output is not charged at all.
AI requests you have in flight count against the balance while they run, at the most they could cost, and are reconciled to their real cost when they finish. Several expensive completions started at once therefore see each other's spend rather than each being told the whole balance is available — the later ones get 402 insufficient_funds if the balance can't cover them all.
Every limit is a rolling window, keyed per user. Where three numbers are shown they are paid / free / anonymous — "paid" is any subscription tier.
Shared by chat, image generation, video, TTS, speech and OCR:
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Requests per 10s (per interface + method) | 200 | 30 | 20 |
| Concurrent requests (per interface + method) | 20 | 3 | 2 |
Concurrency is counted per interface, so an image generation and a chat completion do not compete for the same slots.
The OpenAI- and Anthropic-compatible endpoints (/puterai/openai/v1/*, /puterai/anthropic/v1/messages) additionally require a paid plan — a free account calling them gets 402 subscription_required. The same models are available to every account through puter.ai.* and /drivers/call, under the limits above; the model catalogue endpoints stay open to everyone.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
get / set / etc. per 10s | 400 | 400 | 200 |
list (prefix scan) per minute | 240 | 120 | 60 |
| Concurrent calls | 30 | 15 | 8 |
Concurrent list | 5 | 3 | 2 |
Sizes are fixed for every account:
| Size | Limit |
|---|---|
| Key | 1 KB |
| Value | 400 KB |
| Any number inside a value | ±9,007,199,254,740,991 (2<sup>53</sup>−1) |
A key or value over its size limit is rejected outright. A number over its limit is not: it is stored clamped to the bound, and NaN is stored as null — the same thing JSON.stringify() does with it. This applies to numbers nested anywhere inside an object or array, so a value carrying one still keeps every other field it holds. Anything that has to stay exact past 2<sup>53</sup> — a large id, a running total — should be stored as a string.
All per minute unless stated:
| Operation | Paid | Free | Anonymous |
|---|---|---|---|
stat | 1,200 | 600 | 300 |
readdir | 600 | 300 | 120 |
readdir burst (per 10s) | 120 | 60 | 30 |
read | 600 | 300 | 120 |
write | 300 | 120 | 30 |
| Multipart upload calls | 2,400 | 1,200 | 600 |
| Mutations (mkdir/rename/delete/move/copy) | 1,200 | 900 | 600 |
| Mutations, sustained (per hour) | 6,000 | 3,000 | 1,800 |
| Search | 60 | 30 | 10 |
space() | 60 | 30 | 15 |
| Sign a URL | 300 | 150 | 60 |
| Concurrency | Paid | Free | Anonymous |
|---|---|---|---|
read | 10 | 5 | 3 |
write | 15 | 6 | 3 |
| Search | 5 | 2 | 2 |
Signed-URL routes have no session to key on, so they are bounded per network rather than per account: 3,000 reads/min, 600 writes/min, 60 concurrent.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Subdomain reads per 10s | 200 | 200 | 100 |
Subdomain create per minute | 120 | 60 | 30 |
| Concurrent subdomain calls | 20 | 10 | 5 |
| Worker metadata reads per minute | 600 | 300 | 150 |
Worker create (deploy) per minute | 120 | 80 | 40 |
Worker destroy per minute | 30 | 20 | 10 |
| Concurrent worker calls | 10 | 5 | 3 |
| Concurrent deploys | 5 | 2 | 2 |
Sharing is bounded twice: on the calls, and on how many people one account can reach in a day.
| Limit | All accounts |
|---|---|
share / revoke calls per minute | 60 |
share / revoke calls per day | 500 |
Reads (getShares, listShared) per minute | 600 |
| New shares per day | 200 |
| Recipients per request | 10 |
| Items per request | 50 |
A "new share" is one that gives someone access they didn't already have. Changing the mode on an existing share, or re-sharing an item the recipient already has, costs nothing. Over the daily limit, share fails with share_daily_limit_reached.
Separately, the notification and email that tell a recipient about a share are budgeted — being told is not the same as being interrupted about it:
| Announcement | Limit |
|---|---|
| From one sender to one recipient | 1 per 15 minutes, 20 per day |
| To one recipient, from anyone | 10 per hour, 50 per day |
Recipients are emailed by default and opt out with the unsubscribe link the mail carries; a deployment can turn share email off entirely with share_email_notifications: false.
Over these, the share still succeeds — only the announcement is dropped. The recipient's notification is kept up to date either way, and folds several senders into one ("alice and bob shared 5 items with you"), so nothing is lost; it just doesn't interrupt them again. Emails are additionally batched: everything triggered for one recipient within a 90-second window goes as a single digest message. Recipients can also refuse shares outright — from one sender, or from everyone — which fails that sender's share call with recipient_not_accepting_shares. Both are managed from Settings → Security → Blocked people.
Every driver call also passes one shared per-account budget of 8,000 calls/min before the per-API limits above. It exists to catch a runaway loop, not to shape normal traffic — a client that sees a 429 from it is looping.
Every account has a byte quota for the filesystem (100 MiB free; paid plans add more). Storage is what the user is keeping, not what they transferred — deleting files frees it immediately. At the limit, writes fail with 413 storage_limit_reached; reads keep working. puter.fs.space() returns { capacity, used } live.
| Status | code | Meaning | What to do |
|---|---|---|---|
429 | too_many_requests | Rate or concurrency limit | Back off and retry; the window is at most 60s (or 1h for the sustained FS budget) |
402 | insufficient_funds | Monthly credit spent | The user buys credit or upgrades; resets next month |
402 | subscription_required | The endpoint is limited to paid plans | The user upgrades — retrying or waiting changes nothing |
413 | storage_limit_reached | Storage quota reached | The user deletes files or upgrades |
Errors come back as JSON: { "error": …, "message": …, "code": … }.
The SDK turns the money-shaped failures into prompts without any code on your part: an AI call that runs out of credit and a filesystem write that runs out of space both surface an upgrade dialog to the user (in an app via puter.ui.requestUpgrade(), on the web as a usage-limit dialog). Everything else rejects the promise with the shape above — an app that writes files should still handle storage_limit_reached explicitly rather than letting a save fail quietly, and anything running a loop should treat 429 as a signal to back off.
puter.fs.space() → { capacity, used } — bytes, live.puter.auth.getMonthlyUsage() → month-to-date spend and the remaining allowance, per API.