docs: publish rate limits and quotas, and make limit changes move the docs (#3568)

Developer-facing reference for every rate limit, concurrency cap,
quota, and error shape — an advanced page, since puter.js already turns
the common failures into prompts. AGENTS.md and CONTRIBUTING.md now
carry the rule that a PR moving any of these numbers updates the page
in the same PR: an undisclosed limit is one developers discover as a
service failure.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Salazar
2026-08-14 00:41:01 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 25a5799088
commit 126ec09556
4 changed files with 130 additions and 0 deletions
+1
View File
@@ -52,6 +52,7 @@ When in doubt, return less. Auth-, permission-, or data-export-related changes d
- **Test new behavior.** Every new function, endpoint, driver method, or logic branch gets a test; every bug fix gets a regression test that fails before the fix. If something is genuinely hard to test, skip it but say so in the PR.
- **Boy Scout Rule, proportional to the change.** Fix the obvious typo or dead import in files you're already touching; don't ride a refactor along with a bug fix.
- **Understand what you commit.** AI assistance is fine; shipping code you couldn't defend in review is not.
- **A limit change is not done until the docs change with it.** Every rate limit, concurrency cap, quota, and allowance in the code is published in [src/docs/src/rate-limits-and-quotas.md](src/docs/src/rate-limits-and-quotas.md) — an undisclosed limit is one developers discover as a service failure. If a PR moves any of these numbers, the same PR updates that page. The numbers live in `src/backend/controllers/fs/limits.ts` (filesystem), `src/backend/drivers/util/aiLimits.ts` (all AI drivers), per-driver `rateLimit`/`concurrent` configs, and `src/backend/data/subPolicies/` (free-tier allowances).
---
+2
View File
@@ -32,6 +32,8 @@ Don't commit code you couldn't have written, debugged, or defended yourself. Rea
If you add or change a public API (an endpoint, driver method, or puter-js method), follow [doc/contributing-apis.md](doc/contributing-apis.md) — backward compatibility, [developer docs](src/docs/), types, and tests all move in the same PR.
The same rule applies to limits: rate limits, concurrency caps, quotas, and allowances are published in [src/docs/src/rate-limits-and-quotas.md](src/docs/src/rate-limits-and-quotas.md), and a PR that changes one of those numbers updates that page in the same PR. A limit nobody published is a limit developers only discover as a service failure.
## 6. Boy Scout Rule — leave it 1% better
![Boy Scout Rule](https://imgs.search.brave.com/DMmIWl5-NuZVtrR9kXBb06AKF8kturkgSW9UMb2-6m4/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9sYXdz/b2Zzb2Z0d2FyZWVu/Z2luZWVyaW5nLmNv/bS9pbWFnZXMvbGF3/cy9ib3ktc2NvdXQt/cnVsZS5wbmc)
+122
View File
@@ -0,0 +1,122 @@
---
title: Rate Limits and Quotas
description: The rate limits, usage credits, and storage quotas that apply to Puter.js apps, and how to handle hitting them.
---
<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:
| 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](/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](https://puter.com/dashboard#usage)).
- Paid plans carry a larger allowance; see the [plans page](https://puter.com/dashboard#billing) 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.
## 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:
| 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.
### Key-value store
| 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 |
### Filesystem
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.
### Sites and workers
| 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 |
### 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
| 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 |
| `413` | `storage_limit_reached` | Storage quota reached | The 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.
+5
View File
@@ -18,6 +18,11 @@ let sidebar = [
source: '/security.md',
path: '/security',
},
{
title: 'Rate Limits and Quotas',
source: '/rate-limits-and-quotas.md',
path: '/rate-limits-and-quotas',
},
{
title: 'User-Pays Model',
source: '/user-pays-model.md',