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, 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. Each app a user runs gets its own bucket, and each worker gets its own on top of that, so a busy worker never rate-limits the same user's other apps.
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 and app (and per worker, for calls made from a worker). 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.
Sharing a file or folder with anyone with the link (puter.fs.share() with { anyone: true }) is a paid-plan feature too: a free account gets subscription_required, and a link stops working while its owner's plan has lapsed. Sharing with named people and teams is open to every account.
puter.ai.txt2img() returns one image per call. Image-specific limits apply in addition to the shared AI limits above:
| Provider | Limit |
|---|---|
| xAI | Up to 5 reference images; larger requests fail with bad_request. |
| Together | Image routes are excluded for required third-party data sharing; generation fails before any upstream call. |
| Cloudflare | Output dimensions are clamped per side: FLUX.2 256–1920; Lucid Origin 64–2500; Phoenix 64–2048; SDXL and Inpainting 256–2048. Schnell is fixed at 1024×1024. One reference image on FLUX.2 and Inpainting models. Schnell: 1–8 steps; Lucid Origin: 1–40; Phoenix and FLUX.2 Dev: 1–50. Klein uses exactly 4 steps; SDXL and Inpainting use 1–20 steps. |
| Replicate | At most 10 references, subject to a model's lower limit; FLUX 1.1 Pro accepts one reference. Each fetched reference is capped at 30 MB. Riverflow accepts at most two fonts. Native options follow each model's schema. For additional models with explicit width/height controls, pixel dimensions round to multiples of 8 and clamp to schema bounds; omitted bounds default to 64–4096 per side. Predictions expire after 10 minutes; polls are 2 seconds apart. Cancellation cleanup polls for up to 30 seconds, plus an in-flight request. Network timeouts: 90 seconds for creation, 30 seconds for other requests. |
| BytePlus | Pro: 10 references; other Seedream models: 14. |
| BytePlus explicit output size | Pro: 921,600–4,624,220 total pixels. Lite and 4.5: 3,686,400–16,777,216. 4.0: 921,600–16,777,216. All require integer dimensions and an aspect ratio between 1:16 and 16:1. Pairs below 921,600 total pixels are interpreted as aspect hints, not explicit output sizes. |
See txt2img() for provider-specific options and supported models.
| 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 (253−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 253 — 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.
getReadURL() and revokeReadURL() are not tiered by plan like the table above:
| Limit | Value |
|---|---|
Create (getReadURL, shared with any other access-token creation) |
20/hour |
Revoke (revokeReadURL) |
60/min |
The Puter desktop generates PDF upload thumbnails locally with these best-effort budgets. Exceeding them skips the preview and does not reject the original file upload:
| PDF thumbnail preparation | Limit |
|---|---|
| Input PDF size | 20 MiB |
| Active PDF renderers per desktop page | 1 |
| Preparation per upload, including queued PDFs | 5 seconds from the first eligible PDF |
| Worker lifetime per PDF, including asset loading and cleanup | 4 seconds |
| Embedded image or intermediate canvas area | 4,194,304 pixels |
| Image resize budget passed to PDF.js | 16 MiB |
| Output | First page, at most 128 × 128 pixels, preserving aspect ratio |
| Thumbnail payload | 2 MiB |
The SDK allows five seconds for each separate signed thumbnail transfer. A failed or timed-out thumbnail transfer is skipped; explicit upload cancellation and failures transferring the original file still stop the upload. These are preview budgets, not upload file-size limits. The PDF renderer's memory budgets do not constitute a hard limit on total browser-process memory.
| Limit | Value |
|---|---|
| Grant / revoke calls | 60/min per account |
| Permissions per grant or revoke request | 16 |
Filesystem entries a create grant may bring into existence per request |
4 |
Path depth a create grant may provision below the home directory |
16 components |
The last two apply to a raw fs: permission request whose path doesn't exist yet, which is created on approval unless create: false is passed. Missing intermediate directories are created along with the requested path.
The dav host authenticates each request itself, so its limits are bounded per network rather than per account: 600 requests/min and 10 concurrent, one ceiling for everyone.
A DAV client resends its credentials on every request, so that ceiling can't also bound credential guessing. Failed sign-ins are counted separately — successful ones cost nothing:
| Limit | Per 15 minutes |
|---|---|
| Failed sign-ins for one account | 10 |
| Failed sign-ins from one address | 50 |
Over either, the host answers 429 until the window rolls off — including for the right password. Ten wrong ones lock that account out of dav for the rest of the window, so a client left running with a stale password keeps itself locked out; fix the stored password and wait for the window rather than retrying.
This applies only to dav. The account is unaffected everywhere else — the desktop, the API and puter.auth all keep working throughout.
Mounting with a -token username and an API token as the password skips the per-account ceiling entirely, which is the better setup for anything long-lived: the token is revocable from the dashboard without changing the account password, and it can't be locked out by someone else guessing at your account.
| 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 |
| Limit | Value |
|---|---|
getProfile reads |
120/min per network |
| Profile writes | 30/min per account |
| Profile picture (data URL) | 512 KiB |
| Display name | 64 characters |
| Bio | 280 characters |
Another user's profile is readable only while that user is on a paid plan; the owner can always read and write their own.
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, listSharedByMe) per minute |
600 |
| New shares per day | 200 |
| Recipients per request | 10 |
| Items per request | 50 |
The read limit is one bucket shared by every share-listing call, so polling one of them spends budget the others need. stat() with returnShares does the same listing work, so it spends from this bucket too, on top of its own stat budget.
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 5-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.
Available only where a deployment has turned teams on. Every team route is bounded on calls, and the team itself is bounded on how much it can create.
| Limit | All accounts |
|---|---|
| Team mutations per minute | 60 |
| Team mutations per day | 500 |
| Team reads per minute | 600 |
| Teams one account may own | 1 |
| Seats one team may provision, free owner | 4 |
| Seats one team may provision, paying owner | 40 |
| Member password resets per day | 20 |
A seat is a real Puter account created by the team and paid for by its owner, so the seat limit is what bounds a team's size. Over it, provisioning fails with seat_limit_reached; over the team limit, creation fails with team_limit_reached. Both carry the limit in fields.limit.
A seat whose team pays for no tier is on the org_seat_free plan: half the ordinary free allowance, usage and rate caps alike (a free account's bySubscription caps apply to every free plan). Without this, provisioning seats would mint full free tiers nobody pays for. A seat on a paid team tier gets that tier's allowance.
A reset returns a temporary password once and never again. It stops working 24 hours after it is issued, so an unused reset expires rather than becoming a standing credential; after that the administrator has to issue a new one. Until the member replaces it, every authenticated request from that account fails with password_change_required — signing in works, but nothing else does until they choose their own password.
Deleting a team frees the owner's slot, but it does not free the seats: the accounts it created still exist, still hold their files, and keep their usernames. They are disabled, not removed — deleting a team is not a way to stop paying for the accounts in it.
Removing a seat for good is a separate, explicit request, and it is refused unless the account is already disabled (account_must_be_disabled_first). That ordering puts a reversible step in front of the only irreversible operation in the feature. There is no restore window: deletion removes the files, returns the username to the pool, and invalidates every credential. Nothing expires a disabled account on a timer — it persists, costing only the bytes it holds, until someone asks for it to go.
Lowering the seat limit never disables anyone. A team already above a reduced limit keeps every account it has and is simply refused new ones until it is back under.
The seat limit follows the owner's plan: a team whose owner pays nothing stops at max_seats_per_team_free, one whose owner is on a paid plan at max_seats_per_team_paid. Buying a plan raises it with no other action — the accounts already there are untouched, and the next provision simply succeeds. A deployment that does not sell seats can set max_seats_per_team instead, which is one flat cap whatever the owner pays and overrides both.
These are per deployment (max_teams_per_user likewise) rather than per team, so raising one moves every team at once.
A team's whole configuration is its name, its handle, and whether its directory is open to apps. In particular there is no sharing policy: a team cannot restrict who its members share with, by domain or otherwise, and there is no control over public links. Members share exactly as any other Puter account does.
Both buckets are per account, not per team, so administering several teams spends one budget, and the read limit is one bucket shared by every listing call. Where a deployment has teams off, puter.teams rejects with not_found rather than too_many_requests.
One write can reach many subscriptions, so events are bounded on both halves: how much you may register, and how much any one event may turn into.
Durable subscriptions and cross-user share handles outlive the connection that made them, so they are the half that varies by plan:
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Durable subscriptions per account | 500 | 100 | — |
| Durable subscriptions per app, per account | 100 | 25 | — |
| Live key-value share handles per account | 500 | 200 | — |
| Live key-value share handles per app, per account | 100 | 50 | — |
A temporary (anonymous) account cannot create durable subscriptions at all — subscribe fails with events_durable_requires_account, and session subscriptions, which live and die with the connection, are the surface it has. Past either cap the call fails with events_subscription_limit; unsubscribing frees a slot immediately. Minting a share handle fails the same way — events_kv_handle_requires_account — and past either handle cap the mint fails with events_kv_handle_limit_reached; revoking frees a slot, and retired handles stay listed without counting. The per-app handle cap bounds each app namespace; a handle minted without naming one answers to the account cap alone.
| Limit | All accounts |
|---|---|
| Subject length | 4,096 characters |
| Subscriptions per connection | 50 |
subscribe / unsubscribe calls per minute |
60 |
| Subscription listings per minute | 120 |
| Subscription listing page size | 200 |
| Key-value share-handle calls per minute | 60 |
| Key-value share-handle listing page size | 200 |
| Missed-event fetches per minute | 120 |
| Events per fetch page | 200 |
| Matched subscriptions per event | 50 |
| Filter evaluations per event | 200 |
| Key-value value inlined in a delivery | 16 KB |
| Broadcast deliveries per minute, per subscription | 600 |
single deliveries per minute, per subscription |
120 |
| Handler invocations per minute, per (account, app) | 60 |
| Acknowledgements per minute | 600 |
| Undelivered deliveries per subscription | 10,000 |
| Undelivered deliveries per suspended subscription | 100 |
| Suspended subscriptions kept for | 30 days |
| Handler invocation timeout | 30 seconds |
| Wait before retrying a failed handler | 2 seconds, doubling |
| Longest wait between retries | 5 minutes |
| Handler failures in a row before suspension | 5 |
| Published handlers per app | 100 |
| Handler source size | 64 KB |
Handlers per publishAll call |
50 |
| Handler publish / remove calls per minute | 60 |
| Handler listings per minute | 120 |
| Events worker listings per minute | 120 |
| Events worker listing page size | 200 |
fetch() reads a page of what a subject recorded rather than a delivery, so it is budgeted with the listings: a page defaults to 50 events and is capped at 200, and a client catching up walks pages until one comes back with no cursor. Only notif: has a store to read — the notification mailbox, kept for as long as the deployment's retention window (deployment-configured, no fixed number here) — and any other subject family is refused with fetch_unsupported_subject.
Subscriptions come in two kinds. A session subscription lives with the connection that made it: it is dropped when the connection closes, and a reconnecting client subscribes again. A durable subscription outlives every connection — it is created over the API, listed and revoked from the account, and keeps delivering until you remove it or it expires.
The 51st subscription on one connection, and the durable subscription past your plan's cap, both fail with events_subscription_limit. Over the call budget, subscribe and unsubscribe fail with too_many_requests. Subscribing to something you cannot read fails with subject_does_not_exist — the same answer as subscribing to something that is not there, so the call cannot be used to find out which.
A durable subscription may carry a context: JSON that is stored with it and handed to its handler on every delivery, capped at a hard 4 KB and rejected over that with events_context_too_large — client-side, before the request. It is stored in plaintext and read only on the delivery path; listings return its key names and a content hash, never its values. For anything larger, store it in a file and put the path in context. An app sees and revokes only the subscriptions it created; a session acting for the account sees them all, including ones left behind by an app that has since been removed.
A durable subscription runs a handler its app published by name. An app may publish 100 of them, each up to 64 KB of source, and a name is unique inside one app. All of an app's handlers combined may not exceed 5 MB of source; a publish that would push the total over that is refused with events_worker_too_large. Publishing is a developer operation: the account has to own the app. Publishing the same source again is a no-op; publishing different source under a name whose current source the caller did not name as its base is refused with events_handler_conflict, so two racing build steps never silently pick a winner — replace: true is how a caller says it means to take the name. Handler source is never returned by any listing.
The first published handler brings up an events worker for that app; the last one removed, or puter.events.workers.destroy(), takes it down. An app's events worker may (re)deploy at most 30 times an hour; past that, delivery stays retriable until the hour rolls over. puter.events.workers.list() shows every app you own that currently has one from an account session or API token, or just its own from an app — see puter.events.workers for details, including how a hosted deployment may bill it.
A subscription can end or stop without you unsubscribing. Access is re-checked against the stored permission on every delivery, so a share that is taken back stops delivering immediately; the subscription is then suspended, with suspendedAt and suspendedReason in list. There are four reasons:
suspendedReason |
Cause | Resumes when |
|---|---|---|
handler_not_found |
The handler it is bound to was removed | The name is published again |
failures |
Its handler failed or timed out repeatedly | The subscription is republished against a working handler |
no_credit |
Its holder ran out of credit | The balance is restored |
permission_revoked |
The grant it was made under was withdrawn | Never — subscribe again |
A suspended subscription stops delivering and stops being metered, so it cannot go on holding a full backlog for free: what it is owed is trimmed to 100 deliveries and given a deadline — 24 hours for handler_not_found and failures, 1 hour for no_credit — after which they are dropped and one gap marker with reason: 'suspended_backlog_expired' takes their place. A subscription suspended by permission_revoked has its backlog purged immediately: it names paths its holder has just lost the right to see, and holding them for a resume that by design never comes would turn a revocation into a delayed disclosure. A suspended row itself is deleted 30 days after it stops.
Deleting the node a subscription is anchored on ends it too, unless the subject named a path or a pattern, in which case it follows that path up to the nearest folder that still exists and keeps watching, so recreating the path resumes delivery.
Match patterns are compiled once when you subscribe and are capped at 256 characters and 16 segments, with one * per segment and one ** per pattern; anything past that is rejected with invalid_subject_pattern. ** crosses directories and costs no more than *.
A kv: subject is indexed on the first 6 :-segments, or 160 bytes, of its key — whichever comes first; past that the remainder becomes a match pattern, which is subject to the caps above. A key-value subject matches its key exactly unless it ends in *, and a * anywhere else — or a ? — is rejected with invalid_kv_pattern. Watching another app's key-value data is refused with events_cross_app_disabled where that is not enabled, and otherwise takes the same consent as reading it. The app slot names an app uid and is capped at 40 characters; past that the subscription is refused with events_value_too_large. A subscription made with includeValue is handed the key's new value on each delivery, up to 16 KB serialized; a larger value is left out of the event and the subscriber reads the key back. A share-handle subscription may ask for values too, and receives them for as long as the handle stands.
Deliveries are coalesced over 250 ms per subject. A multipart upload, a save loop, or a recursive delete is one thing the user did, and it arrives as one event carrying the newest state rather than as one event per write. Two different files in the same window are two deliveries.
The two per-event ceilings — matched subscriptions and filter evaluations — do not fail your call: they truncate the delivery and send a gap marker in its place, with reason: 'matched_subscription_limit' or reason: 'filter_evaluation_limit' respectively — an event with op: 'gap' and no uid or path. A gap means something happened that you were not told the details of, so a client that must not miss changes should re-read the anchor when it sees one rather than treat the silence as "nothing changed". Both ceilings are counted per region: a change is evaluated against every matching region's own copy of your subscriptions, so an account with subscribers spread across several regions can see more than 50 matched, or 200 evaluated, in total for one event, even though no single region ever exceeds its own cap.
A background delivery — one that runs your app's handler with nobody there — takes the user's consent, the per-app permission events:background, and a subscription targeting worker without it is refused with events_background_consent_required. The handler runs as your app's own session for that user — the same reach it has from a tab, not a credential cut down to this one subscription's grant — and that session is what the consent authorizes running unattended; it shows up in the user's sessions list as a worker session, and revoking it there stops every background delivery for your app the same way withdrawing the permission does. Destroying the app's events worker (puter.events.workers.destroy()) retires that session too, and deleting the app ends it along with every subscription and anything they were owed. A handler has 30 seconds to answer each invocation. Answering 2xx takes the delivery; 4xx refuses it, and it is dropped with a gap marker carrying reason: 'handler_rejected' rather than sent again to the same answer; 5xx, 429 and a timeout are all "not now", and the delivery is held 2 seconds before the next attempt, doubling each time up to 5 minutes. Five failures in a row — refusals included — suspend the subscription with failures, hold what it is owed under the suspended-backlog rules above, and notify the app's developer. Publishing a handler is all the deployment there is: the app's events worker is brought up the first time a delivery needs it, and again if it has been idle long enough to be evicted, so the first background delivery after a publish pays a short cold start. Nothing else can invoke it — it answers one platform route, and only the platform can reach it.
A single subscription is delivered to exactly one consumer, which has 60 seconds — twice the handler invocation timeout, so a slow but successful handler is never re-invoked mid-run — to acknowledge each delivery before it is offered again, twice to a connected client and then to the subscription's handler. Until it is acknowledged it is held for you, so a consumer that is away is a backlog that grows: 10,000 undelivered deliveries per subscription, after which the oldest are dropped and one gap marker with reason: 'backlog_overflow' takes their place. Each region also holds at most 1,000,000 undelivered deliveries across every subscription it serves, and sheds the oldest first — with the same marker — before it reaches that. A redelivery after a missed acknowledgement is normal and expected: deliveries are at-least-once, event.id is stable across them, and a handler that runs twice on the same id should do nothing the second time.
Both per-minute delivery budgets are spent per subscription and answered with a gap marker carrying reason: 'delivery_rate_limit' rather than an error. The handler budget is different: a delivery that arrives when its app has spent the minute's invocations is not failed and does not count as a handler failure — it stays owed and goes out on a later attempt.
Deliveries are metered to the subscription's holder — your data, your subscriptions, your bill. A subscription that sits idle costs nothing; the plan quotas above are what bound how many you can hold.
| Line | Rate | Counted per |
|---|---|---|
events:delivery:broadcast |
10 µ¢ | delivered event |
events:delivery:single |
100 µ¢ | delivered event |
A single costs more because it is leased and acknowledged; a broadcast copy is a socket write. Handler runs bill separately through the usual worker path.
Only deliveries that actually happen are billed. An event a filter excluded, several writes the 250 ms window collapsed into one, a delivery a permission re-check stopped, and every gap marker are all free — a marker says something was lost, and charging for the loss would be charging you twice. Session subscriptions are billed at the broadcast rate like any other.
Deliveries stop when the holder's balance runs out: the subscription is suspended with suspendedReason: 'no_credit', the holder is notified, and nothing further is metered against it. What it was owed is held for 1 hour. Restoring the balance resumes it — checked periodically rather than the instant a payment lands, so allow a few minutes after topping up.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Relay credentials per minute | 30 | 10 | 5 |
| Guest grants issued per minute | 30 | 10 | 5 |
Signalling details are public deployment config and bounded per network instead of per account, at 3,000 reads/min.
Guests are bounded per host: everyone holding grants from the same account shares 60 relay-credential requests/min. Relay traffic a guest sends is metered against the account that issued the grant, so treat a grant as something that spends your allowance — issue it for the session you meant to host, and let it expire rather than reusing one indefinitely.
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: a call that runs out of credit (insufficient_funds), one the user's plan doesn't include (subscription_required), and a filesystem write that runs out of space (storage_limit_reached) all surface an upgrade dialog to the user — in an app via puter.ui.requestUpgrade(), on the web as a dialog the SDK renders itself. The dialog names the call that was refused, and for a plan gate says what needs the plan: the SDK's own wording where it has one (email), otherwise the message the server sent. The promise still rejects with the shape above — an app that writes files should 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.