Back to Puter

Rate Limits and Quotas

src/docs/src/rate-limits-and-quotas.md

26.08.29.7 KB
Original Source
<div class="info">This is an advanced reference. Puter.js already handles the common cases for you — a call that runs out of credit or storage surfaces an upgrade prompt to the user automatically, and most apps never need the numbers on this page. Read on if you're designing for high request volumes or want to handle limit errors yourself.</div>

Three separate mechanisms decide whether a call succeeds. They are independent, and hitting any one of them is enough to stop a request:

MechanismBoundsRefillsFailure
Usage creditwhat usage costs (AI, egress, KV capacity, storage ops, workers)monthly, per plan402 insufficient_funds
Rate limithow many requests are made per windowrolling window (10s / 1min / 1h)429 too_many_requests
Storage quotahow many bytes are kept in the filesystemnever — the user deletes or upgrades413 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 credit

Usage is charged against the account's monthly credit allowance, metered per operation at real cost.

  • Every account starts with a free monthly allowance (shown in the dashboard).
  • Paid plans carry a larger allowance; see the plans page for current tiers.
  • Allowances reset monthly and do not roll over. Purchased top-up credits never expire and are spent after the allowance is gone.

What usage costs (the big three):

  • Egress — every byte sent to a client, on all responses, not just file downloads. This is the one developers underestimate.
  • AI — priced per model and per token/second/character. 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.
  • KV and storage operations — small per-operation costs; reads served from cache are charged a fraction of an uncached read.

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.

Rate limits

Every limit is a rolling window, keyed per user. Where three numbers are shown they are paid / free / anonymous — "paid" is any subscription tier.

AI

Shared by chat, image generation, video, TTS, speech and OCR:

LimitPaidFreeAnonymous
Requests per 10s (per interface + method)2003020
Concurrent requests (per interface + method)2032

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.

Key-value store

LimitPaidFreeAnonymous
get / set / etc. per 10s400400200
list (prefix scan) per minute24012060
Concurrent calls30158
Concurrent list532

Sizes are fixed for every account:

SizeLimit
Key1 KB
Value400 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.

Filesystem

All per minute unless stated:

OperationPaidFreeAnonymous
stat1,200600300
readdir600300120
readdir burst (per 10s)1206030
read600300120
write30012030
Multipart upload calls2,4001,200600
Mutations (mkdir/rename/delete/move/copy)1,200900600
Mutations, sustained (per hour)6,0003,0001,800
Search603010
space()603015
Sign a URL30015060
ConcurrencyPaidFreeAnonymous
read1053
write1563
Search522

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.

Sites and workers

LimitPaidFreeAnonymous
Subdomain reads per 10s200200100
Subdomain create per minute1206030
Concurrent subdomain calls20105
Worker metadata reads per minute600300150
Worker create (deploy) per minute1208040
Worker destroy per minute302010
Concurrent worker calls1053
Concurrent deploys522

Sharing

Sharing is bounded twice: on the calls, and on how many people one account can reach in a day.

LimitAll accounts
share / revoke calls per minute60
share / revoke calls per day500
Reads (getShares, listShared) per minute600
New shares per day200
Recipients per request10
Items per request50

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:

AnnouncementLimit
From one sender to one recipient1 per 15 minutes, 20 per day
To one recipient, from anyone10 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.

Everything at once

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.

Storage quota

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.

What happens when you hit a limit

StatuscodeMeaningWhat to do
429too_many_requestsRate or concurrency limitBack off and retry; the window is at most 60s (or 1h for the sustained FS budget)
402insufficient_fundsMonthly credit spentThe user buys credit or upgrades; resets next month
402subscription_requiredThe endpoint is limited to paid plansThe user upgrades — retrying or waiting changes nothing
413storage_limit_reachedStorage quota reachedThe user deletes files or upgrades

Errors come back as JSON: { "error": …, "message": …, "code": … }.

What Puter.js already does for you

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.

Checking usage from your app

  • puter.fs.space(){ capacity, used } — bytes, live.
  • puter.auth.getMonthlyUsage() → month-to-date spend and the remaining allowance, per API.