Three independent checks can stop a request:
| Check | What it limits | Resets | Error |
|---|---|---|---|
| Usage credit | What usage costs (AI, egress, KV, storage operations, workers) | Monthly, per plan | 402 insufficient_funds |
| Rate limit | How many requests are made in a window | Rolling window (10 s, 1 min or 1 h) | 429 too_many_requests |
| Storage quota | How many bytes are stored | When the user deletes files or upgrades | 413 storage_limit_reached |
Credit doesn't buy rate-limit headroom, and an empty balance doesn't block reads that cost nothing.
Under the User-Pays Model, limits are counted against each user's own account, so one heavy user can't use up your app for everyone else. Every limit on this page is counted in one of these scopes:
| Scope | What shares one budget |
|---|---|
| Per user, per app | One user in one app. This is the default: a limit that names no scope uses it. The same user in another app has a separate budget. The Puter desktop and API tokens count as one more app, and each worker gets its own budget too. |
| Per user, all apps | One user, summed across every app they use. Used for quotas on things an account keeps (subscriptions, share handles, shares per day, uploads in progress) and a few call budgets. |
| Per app, all users | One app, summed across all of its users. Used for things a developer publishes, such as event handlers. |
| Per network | One IP address or device. Used by routes with no signed-in user, such as signed URLs and WebDAV. |
When a quota has both a per-app and an all-apps number, both apply: one app can use up to its per-app share, and all apps together can use up to the all-apps total.
Where a table shows three numbers, they are paid / free / anonymous. Paid is any subscription tier. Anonymous is a temporary (guest) account.
Usage is charged against the account's monthly allowance at real cost.
The main costs:
puter.ai.listModels() lists the models, and GET /metering/allCosts returns the per-model rates.For AI:
402 insufficient_funds; a per-second video is first shortened to what the balance still covers.All rate limits are rolling windows, counted per user, per app unless a scope is given.
Shared by chat, image generation, video, TTS, speech and OCR. Each interface and method has its own budget, so an image generation and a chat completion don't compete.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Requests per 10 s | 200 | 30 | 20 |
| Concurrent requests | 20 | 3 | 2 |
| Input | Limit |
|---|---|
Text for txt2speech() |
3,000 characters |
Audio for speech2speech() |
25 MiB |
| Image inlined in a chat message | 5 MB |
| File attached to a chat message, Claude models | 30 MB |
| File attached to a chat message, OpenAI models | 5 MB |
An input over its limit is rejected before it reaches the model, as is an image URL that doesn't finish downloading within 30 seconds. OCR input limits are under OCR.
Text-to-speech calls to ElevenLabs, xAI and Speechify time out after 2 minutes; xAI speech-to-text and voice-changer calls after 10 minutes, upload included. A call that times out before any audio or transcript comes back fails with 504 upstream_timeout.
The OpenAI- and Anthropic-compatible endpoints (/puterai/openai/v1/*, /puterai/anthropic/v1/messages) require a paid plan; a free account gets 402 subscription_required. The same models are available to every account through puter.ai.* and /drivers/call, and the model catalogue endpoints are open to everyone.
/puterai/anthropic/v1/messages/count_tokens has its own budget of 120 requests per minute per user, with no concurrency limit, and is not charged.
Claude's server-executed tools are clamped so a request can't reserve an unbounded amount of credit:
| Tool | Limit |
|---|---|
Web search / web fetch max_uses |
Defaults to 10 when omitted; capped at 20. |
Advisor max_uses |
Defaults to 3; capped at 10. |
Advisor max_tokens |
Defaults to 16,384; capped at 32,768. |
A web search is metered at a flat per-request rate in addition to the tokens it reads back; an advisor call is metered under the named advisor model's own rates (an advisor model outside the catalog is priced at the most expensive entry, never left unpriced).
puter.ai.txt2img() returns one image per call. These limits apply on top of the AI limits above:
| Provider | Limit |
|---|---|
| All | Up to 16 reference images; more fails with bad_request. A reference given as a URL is fetched with a 30-second timeout and capped at 30 MB. |
| xAI | Up to 5 reference images; more fails with bad_request. |
| Together | Image routes are excluded because they require 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. Steps: Schnell 1–8; Lucid Origin 1–40; Phoenix and FLUX.2 Dev 1–50; Klein exactly 4; SDXL and Inpainting 1–20. |
| Replicate | At most 10 references, or the model's lower limit; FLUX 1.1 Pro accepts one. Each fetched reference is capped at 30 MB. Riverflow accepts at most two fonts. Native options follow each model's schema. For other models with width/height controls, dimensions round to multiples of 8 and clamp to the schema bounds (64–4096 per side if none are given). Predictions expire after 10 minutes and are polled every 2 seconds. 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. Sizes under 921,600 total pixels are treated as aspect-ratio hints, not exact sizes. |
See txt2img() for provider-specific options and supported models.
For chat providers that require remote image URLs to be inlined (currently Moonshot):
| Limit | Value |
|---|---|
| Remote images per request | 20 |
| Concurrent image fetches per request | 4 |
| Image bytes | 5,000,000 bytes per image |
| Fetch timeout, including the body | 30 seconds per image |
Excess images and failed fetches become text error parts. Remote bodies are stopped
as soon as the byte cap is exceeded. Oversized AI file inputs return
413 input_too_large; this does not indicate a storage quota failure.
puter.ai.img2txt() input limits apply in addition to the shared AI limits above:
| Provider | Limit |
|---|---|
| AWS Textract | 10 MB per input. JPEG, PNG, TIFF, or a single-page PDF. |
| Mistral | 50 MB per input. PDFs up to 1,000 pages. |
File, Blob and data URI inputs are checked by the SDK before upload: 10 MB for Textract, and 36 MB when a Mistral model or provider is named, since the base64 upload must fit the 50 MB request body. URLs and Puter paths are read up to the provider's limit and rejected with 413 input_too_large beyond it. See img2txt() for models and options.
Before the provider runs, the balance must cover every page the call can be billed for, or it fails with 402 insufficient_funds. A PDF counts its own pages, or only the pages selected when that is fewer. An image, and any Textract input, counts as one page. Other documents, and PDFs whose pages can't be read, count 20 pages per MB, up to 1,000. That amount is reserved while the call runs; the charge is for the pages actually processed.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
get / set / etc. per 10 s |
400 | 400 | 200 |
list (prefix scan) per minute |
240 | 120 | 60 |
| Concurrent calls | 30 | 15 | 8 |
Concurrent list |
5 | 3 | 2 |
Sizes are the same for every account:
| Size | Limit |
|---|---|
| Key | 1 KB |
| Value | 400 KB |
| Any number inside a value | ±9,007,199,254,740,991 (253−1) |
Path nesting (add/update/incr/decr/remove) |
31 levels |
| Path segments in one call, all paths together | 1,500 |
| Nesting depth of a value, counting its path | 32 levels |
A key or value over its size limit is rejected. A number out of range is not rejected: it's stored clamped to the bound, and NaN is stored as null (as JSON.stringify() does). This applies at any depth inside an object or array. Store values that must stay exact past 253, such as large ids or running totals, as strings.
A path such as a.b.c may chain at most 31 levels, and one call's paths at most 1,500 segments together. All of a call's paths go into one write of limited size, so in practice a call fits about 140 short paths (about 60 for incr and decr), fewer when the paths are long. A value nests at most 32 levels deep: the stored value is the first level, and each object or array inside it, or path segment above it, adds one, so { a: { b: 1 } } stored with set() is 3 levels deep and written by update() at x.y is 5; add() counts the list it appends to as one more. Past any of these, the call rejects with bad_request.
Each call is also capped, the same for every account:
| Per call | Limit |
|---|---|
Keys in one get() |
1,000 |
Items in one batch set() |
1,000 |
limit on list() |
1,000 |
offset on list() |
5,000 |
A get() or batch set() over its cap, or an offset over 5,000, is rejected with bad_request and nothing is read or written. A limit over 1,000 is lowered to 1,000, and the page's cursor picks up from there.
Per minute unless stated:
| Operation | Paid | Free | Anonymous |
|---|---|---|---|
stat |
1,200 | 600 | 300 |
readdir |
600 | 300 | 120 |
readdir burst (per 10 s) |
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 |
| Limit | Value | Scope |
|---|---|---|
| Uploads in progress | 10,000 | Per user, all apps |
Entries in one readdir response |
10,000 (1,000 per page by default when paginated or recursive) |
Per request |
Levels a recursive readdir descends |
10 | Per request |
Files and folders in one startBatchWrite, completeBatchWrite or batchWrite |
500 | Per request |
Paths in one delete() |
500 | Per request |
Entries in one /sign request |
500 | Per request |
Part numbers in one signMultipartParts |
10,000 | Per request |
Legacy /batch request |
256 multipart parts (fields and files combined) or 256 JSON operations, 64 files, 100 MiB per file, 1 MiB per field, 256 MiB in total | Per request |
| Signed-URL reads | 3,000/min | Per network |
| Signed-URL writes | 600/min | Per network |
| Signed-URL concurrent requests | 60 | Per network |
getReadURL() (shared with all access-token creation) |
20/hour | |
revokeReadURL() |
60/min |
Non-write operations (mkdir/shortcut/move/delete) inside a legacy /batch request also count against the Mutations limits above.
A request over one of the per-request caps fails as a whole before any item is processed, with 400 bad_request (413 for legacy /batch). puter.fs.upload() already splits larger uploads into requests of 500.
An upload is in progress from startWrite/startBatchWrite (which puter.fs.upload() and puter.fs.write() use) until it completes, is cancelled, or expires. The count includes uploads by collaborators into the account's folders. Over the limit, new uploads fail with 429 too_many_requests until some finish.
The Puter desktop makes PDF upload thumbnails locally within these budgets. Going over one skips the preview; the upload itself is unaffected.
| PDF thumbnail | Limit |
|---|---|
| Input PDF size | 20 MiB |
| PDF renderers per desktop page | 1 at a time |
| Preparation per upload, including queued PDFs | 5 seconds from the first eligible PDF |
| Renderer lifetime per PDF, including 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, aspect ratio preserved |
| Thumbnail payload | 2 MiB |
| Thumbnail transfer | 5 seconds each |
| Limit | Value |
|---|---|
| Grant / revoke calls | 60/min |
| Permissions per grant, revoke, or access-token request | 16 |
extra / meta on a grant or access-token permission |
4 KiB each |
Filesystem entries one create grant may create per request |
4 |
Path depth a create grant may create below the home directory |
16 components |
The last two apply to an fs: permission request for a path that doesn't exist yet, which is created on approval along with any missing parent directories, unless create: false is passed.
| Limit | Value | Scope |
|---|---|---|
| Requests | 600/min | Per network |
| Concurrent requests | 10 | Per network |
| Failed sign-ins | 10 per 15 min | Per account being signed into |
| Failed sign-ins | 50 per 15 min | Per network |
| Lock timeout | Up to 1 hour | Per lock |
Successful sign-ins don't count. Over either sign-in limit, dav answers 429 until the window passes, even for the right password, so a client retrying a stale password keeps itself locked out: fix the password, then wait. This only affects dav; the desktop, the API and puter.auth keep working.
A LOCK gets the Timeout the client asks for, up to 1 hour, and the response states the timeout granted. Refresh the lock before it runs out to keep it.
For long-lived mounts, sign in with a -token username and an API token as the password. That skips the per-account sign-in limit, and the token can be revoked from the dashboard without changing the account password.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Subdomain reads per 10 s | 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 |
| Operation | Paid | Free | Anonymous |
|---|---|---|---|
puter.apps.get() / puter.apps.list() |
100/10 s | 100/10 s | 50/10 s |
REST reads (GET /apps, GET /apps/:names, POST /query/app, record open) |
1,800/min | 1,800/min | 1,800/min |
| Writes (create, update, delete) | 240/min | 120/min | 60/min |
| Limit | Value | Scope |
|---|---|---|
nameAvailable |
60/min | |
| App icon | 12,000/min | Per network |
App landing page (/app/<name>, /desktop/app/<name>) |
600/min | Per network |
Names per batch app lookup (GET /apps/:names, POST /query/app) |
200, up to 200 characters each | |
metadata |
16 KiB, measured as JSON | |
filetypeAssociations |
200 entries, 60 characters each |
An app's name and title may be up to 100 characters and its description up to 7,000. A create or update over any of these is rejected with 400 bad_request; nothing is truncated.
| Limit | Value |
|---|---|
getProfile reads |
120/min, per network |
| Profile writes | 30/min |
| 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. Owners can always read and write their own.
puter.email.sendTransactional() requires a paid plan.
| Limit | Value |
|---|---|
| Sends | 600/min |
| Concurrent sends | 20 |
| Recipients per send (to + cc + bcc) | 10 |
| Attachments per send | 10, up to 25 MB in total |
| Limit | Value | Scope |
|---|---|---|
share / revoke calls |
60/min, 500/day | |
Reads (getShares, listShared, listSharedByMe) |
600/min | |
| New shares | 200/day | Per user, all apps |
| Recipients per request | 10 | |
| Items per request | 50 |
stat() with returnShares (on top of its own stat budget).share fails with share_daily_limit_reached.puter.fs.share() with { anyone: true }) requires a paid plan. A free account gets subscription_required, and the link stops working while the owner's plan has lapsed. Sharing with named people and teams is open to every account.The notification and email that tell a recipient about a share have their own limits:
| 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 |
Over these, the share still succeeds; only the announcement is skipped. The recipient's notification still updates and groups senders ("alice and bob shared 5 items with you"). Emails triggered for one recipient within 5 seconds are sent as one digest.
Recipients can opt out of share email with the link in the email, and a deployment can turn it off with share_email_notifications: false. Recipients can also block shares from one sender or from everyone (Settings → Security → Blocked people); the sender's share then fails with recipient_not_accepting_shares.
Available only where the deployment has teams turned on; elsewhere puter.teams rejects with not_found.
| Limit | Value |
|---|---|
| Team mutations | 60/min, 500/day |
| Team reads | 600/min |
| Teams one account may own | 1 |
| Seats per team, free owner | 3 |
| Seats per team, paid owner | Set by the owner's plan; 40 if the plan sets none |
| Member password resets | 20/day |
The mutation and read budgets are per user, per app, not per team: administering several teams spends one budget.
seat_limit_reached; over the team limit, creation fails with team_limit_reached. Both errors include the limit in fields.limit.org_seat_free plan, which gets half the free allowance and the free rate limits. A seat on a paid team tier gets that tier.max_seats_per_team_free and max_seats_per_team_paid set the two seat limits; a plan registered with teamSeatCap uses its own instead. max_seats_per_team sets one flat limit that overrides all of them. max_teams_per_user sets the team limit. These apply to every team on the deployment.password_change_required.two_factor_required. The owner needs 2FA to turn this on, and can clear a member's second factor if they lose their device; the reset is logged and the member is emailed.account_must_be_disabled_first otherwise). Deletion is permanent, with no restore window: files, username and credentials are all removed. Disabled accounts are never deleted automatically.See Events for an overview, onLocal() for gap markers, and onPersistent() for suspension.
| Limit | Scope | Paid | Free | Anonymous |
|---|---|---|---|---|
| Persistent subscriptions | Per user, all apps | 500 | 100 | 0 |
| Persistent subscriptions | Per user, per app | 100 | 25 | 0 |
| KV share handles | Per user, all apps | 512 | 200 | 0 |
| KV share handles | Per user, per app | 512 | 128 | 0 |
| Session subscriptions | Per connection | 50 | 50 | 50 |
events_subscription_limit. Over a share-handle limit: events_kv_handle_limit_reached.events_durable_requires_account for persistent subscriptions and events_kv_handle_requires_account for share handles.Counted per event, per region:
| Limit | KV, paid owner | KV, free or anonymous owner | Other events |
|---|---|---|---|
| Subscriptions that receive the event | 512 | 128 | 50 |
| Subscription filters checked | 2,048 | 512 | 200 |
includeValue values are left out of every delivery for a change when more than 128 subscriptions match it (whatever the owner's plan), when the filter-check limit stops the count early, or when the value is over 16 KB.| Call | Limit | Scope |
|---|---|---|
subscribe |
60/min | Per user, all apps |
unsubscribe |
600/min | Per user, all apps |
| Acknowledgements | 600/min | Per user, all apps |
| Share-handle mint / revoke | 60/min | Per user, all apps |
| Handler publish / remove | 60/min | Per user, all apps |
| Subscription and share-handle listings (combined) | 120/min | Per user, per app |
| Handler listings | 120/min | Per user, per app |
| Events worker listings | 120/min | Per user, per app |
fetch() |
120/min | Per user, per app |
Over any of these: too_many_requests. Listing pages hold up to 200 items. A fetch() page defaults to 50 events and holds up to 200.
Unsubscribing counts toward its own unsubscribe limit, not the subscribe one.
| Limit | Value | Scope |
|---|---|---|
| Broadcast deliveries | 600/min | Per subscription |
single deliveries |
120/min | Per subscription |
| Handler runs | 60/min | Per user, per app |
| Coalescing window | 250 ms | Per subscription and subject |
Time to acknowledge a single delivery |
60 seconds | Per delivery |
| Undelivered backlog | 10,000 | Per subscription |
| Undelivered backlog | 1,000,000 | Per region |
| KV value inlined in a delivery | 16 KB | Per delivery |
delivery_rate_limit).single delivery waits and goes out later, and a broadcast one runs only in connected clients. Neither counts as a handler failure.backlog_overflow).| Limit | Value | Scope |
|---|---|---|
| Published handlers | 100 | Per app, all users |
| Source per handler | 64 KB | Per handler |
| Source for all handlers combined | 5 MB | Per app, all users |
Handlers per publishAll() call |
50 | Per call |
| Events worker deploys | 30/hour | Per app, all users |
| Run timeout | 30 seconds | Per run |
| Retry delay | 2 seconds, doubling, up to 5 minutes | Per delivery |
| Failures in a row before suspension | 5 | Per subscription |
user token lifetime in the events worker |
15 minutes | Per run |
Failures count as in a row until the handler takes a delivery or an hour passes without another failure; either resets the count to zero.
Over the deploy limit, deliveries stay queued and retry after the hour rolls over.
| Limit | Scope | Paid | Free |
|---|---|---|---|
| Handler runs in one chain | Per account holding the subscription | 12 | 4 |
user in the events worker, or through a token user creates, is one run deeper than the event that ran the handler. Writes from anywhere else start a new chain. user.workers.create is refused rather than counted — a handler cannot start an untracked chain through a worker of its own.broadcast one still reaches connected clients without running the persistent handler; a single one is still offered to a connected client first and runs there. The dropped run leaves no gap marker and doesn't count as a handler failure.| Limit | Value |
|---|---|
| Backlog kept | 100 deliveries |
| How long the backlog is kept | 24 hours for handler_not_found and failures; 1 hour for no_credit; dropped immediately for permission_revoked |
| Suspended subscription deleted after | 30 days |
When a kept backlog expires, it's replaced by one gap marker (suspended_backlog_expired).
| Limit | Value |
|---|---|
| Subject length | 4,096 characters |
| Match pattern | 256 characters and 16 segments; one * per segment, one ** in total |
| KV key indexed on | The first 6 :-segments or 160 bytes, whichever is shorter; the rest is matched as a pattern |
context on a persistent subscription |
4 KB |
Over a pattern limit: invalid_subject_pattern. Over the context limit: events_context_too_large.
Deliveries are billed to the account holding the subscription:
| Line | Rate | Counted per |
|---|---|---|
events:delivery:broadcast |
10 µ¢ ($0.10 per million) | Delivered event |
events:delivery:single |
100 µ¢ ($1 per million) | Delivered event |
µ¢ is a microcent (a millionth of a cent). Session subscriptions bill at the broadcast rate. Handler runs bill separately as worker usage.
Free: idle subscriptions, events a filter excluded, writes merged by coalescing, deliveries stopped by a permission check, deliveries with no client connected to receive them and no handler run, and gap markers.
When the holder's balance runs out, deliveries stop and persistent subscriptions are suspended with no_credit, and the holder is notified. The backlog is kept for 1 hour. Topping up resumes them within a few minutes.
puter.net sockets connect through a relay that takes a single-use token. A page fetches one token when it opens its first socket and reuses the connection for later sockets; puter.net.generateWispV1URL() fetches a new one on every call.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Relay tokens per minute | 60 | 30 | 10 |
The relay checks each token against Puter, at most 300 checks/min per relay IP.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Relay credentials per minute | 30 | 10 | 5 |
| Guest grants issued per minute | 30 | 10 | 5 |
| Limit | Value | Scope |
|---|---|---|
| Signalling details reads | 3,000/min | Per network |
| Relay credentials for guests | 60/min | Shared by all guests of one host account |
Relay traffic a guest sends is billed to the account that issued the grant. Issue a grant for the session you mean to host and let it expire rather than reusing it.
| Limit | Paid | Free | Anonymous |
|---|---|---|---|
| Open realtime connections per account | 400 | 200 | 100 |
An account can also hold at most 150 open connections from one origin, so a single page can't use up the whole allowance. Over either limit, the new connection is closed as soon as it opens; connections already open stay up.
Every driver call also counts toward 8,000 calls/min, before the per-API limits above. It only exists to catch runaway loops; a client that hits it is looping.
Every account has a filesystem quota (100 MiB free; paid plans add more). It counts bytes stored, not bytes transferred, and deleting files frees space immediately. At the limit, writes fail with 413 input_too_large; reads keep working. puter.fs.space() returns { capacity, used }.
An upload reserves its declared size, minus the size of any file it replaces, from the moment it starts. The reservation is released when the upload completes (the real size counts instead), is cancelled, or expires (15 minutes after starting by default, at most 1 hour, plus 5 minutes' grace). An abandoned upload holds its space until it expires. A startBatchWrite that doesn't fit fails as a whole, up front. space() counts stored bytes only, not reservations.
Puter.js stops a request that makes no progress for too long and rejects it with code: "request_timeout". The clock restarts whenever the request moves: a state change, response bytes arriving, or upload progress where the runtime reports it. A download or stream that keeps moving is never cut off, however long it takes. The file contents puter.fs.upload() and puter.fs.write() send are not subject to this limit.
| Request | Stopped after this long without progress |
|---|---|
Reads: read-only calls such as puter.kv.get(), puter.kv.list(), puter.apps.get() and puter.hosting.list() |
60 s |
Everything else, including writes, AI calls, streamed responses and every puter.fs call |
15 min |
A read is held to 60 s only while progress can be seen: before the response headers arrive, and after them once the body has started arriving. Node.js, service workers and Puter Workers report no progress for a non-streamed body, so there a read moves to the 15 min limit once its headers are in.
A read that times out is retried once. A write or AI call that times out is never retried, because the server may already have done the work: check before repeating it. A stream that stalls throws { message, code: "request_timeout" } from its loop.
| Status | code |
Meaning | What to do |
|---|---|---|---|
429 |
too_many_requests |
Rate or concurrency limit | Back off and retry. Windows are at most 60 s, or 1 h for sustained filesystem mutations. The uploads-in-progress limit clears only when uploads finish, are cancelled, or expire. |
402 |
insufficient_funds |
Monthly credit spent | The user buys credit or upgrades; the allowance resets next month. |
402 |
subscription_required |
The endpoint requires a paid plan | The user upgrades. Retrying or waiting doesn't help. |
413 |
storage_limit_reached |
Storage quota reached | The user deletes files or upgrades. |
| none | request_timeout |
No progress within the request timeout | Retry if the call is safe to repeat. A write or AI call may already have run. |
Errors are JSON: { "error": …, "message": …, "code": … }.
For insufficient_funds, subscription_required and storage_limit_reached, the SDK shows the user an upgrade dialog automatically: through puter.ui.requestUpgrade() in an app, or a dialog the SDK renders on the web. The dialog names the refused call and, for a plan gate, what needs the plan (the SDK's own wording where it has one, otherwise the server's message).
The promise still rejects with the error above. An app that writes files should handle storage_limit_reached so a save doesn't fail silently, and anything running in a loop should back off on 429.
puter.fs.space() → { capacity, used } in bytes, live.puter.auth.getMonthlyUsage() → month-to-date spend and remaining allowance, per API.Total spend and remaining allowance are always current. The per-API and per-app breakdowns can lag by up to about a minute. Past 5,000 distinct APIs in a month, the rest are grouped under other.