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. Gemini Omni video is the exception. Its reservation is an estimate, because Google can return more output tokens than requested, and it settles at the tokens actually used even when they cost more (see Video generation).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 |
Text for txt2speech(), Kling |
1,000 characters |
Gemini instructions for txt2speech() |
3,000 characters |
Audio for speech2speech() |
25 MiB |
Audio for speech2txt(), Gemini |
10 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.
The standard chat timeout is 10 minutes for a response to start, or to finish if it isn't streamed. Stream durations depend on the provider. Native Gemini applies the 10-minute timeout to the whole response, including streaming. Groq uses 60 seconds to start a response. Mistral streams have no timeout of their own. If the caller disconnects, a stream that's still running gets 10 more minutes to finish, and is then stopped. See the AI model directory for model specifications.
GPT-6.1 Terra has a 1,050,000-token context window and a maximum output of 128,000 tokens. Requests with more than 272,000 input tokens are charged at twice the input and cache rates and 1.5 times the output rate for the whole request.
A gemini-4-argon response that isn't streamed is capped at 32,768 output tokens, and a larger max_tokens is lowered to that cap. Streaming uses the model-directory limit.
Text-to-speech calls to ElevenLabs, xAI, Speechify, Kling and Gemini 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. For Gemini, the 2 minutes cover the whole reply, audio included.
A Gemini text-to-speech reply is capped at 64 MiB, enough for the full audio budget. A readable Gemini text-to-speech reply is charged for the token usage it reports, even when it fails or returns no audio. A reply that times out or exceeds the cap reports no usage, so it is charged the full reservation and fails with 504 upstream_timeout or 400 input_too_large; split long text into shorter requests. Requests with an unreadable reply are not charged.
Every Gemini text-to-speech request reserves credit for the models' full published budget of 8,192 input tokens and 16,384 audio tokens, at the model's current rate. Each request also limits Gemini's output to 16,384 tokens. An incomplete reply fails with 502 upstream_failed. If the reply is readable within the size and time limits, it is charged for the usage it reports. The reservation is the same for any text and instructions, in any language. A request fails with 402 insufficient_funds unless your available credit covers the whole reservation, even when the text is short. When the request ends, the unused part is released and you're charged only for the usage Gemini reports. The reservation isn't a cap on the charge. If Gemini reports more tokens than its published limits, the charge follows the report.
Gemini speech-to-text first counts the audio's tokens, which times out after 30 seconds and reads at most 64 KiB of reply. The transcription itself times out after 10 minutes, upload and reply included. A transcript is limited to 8,192 output tokens and its reply to 2 MiB. A longer transcript or larger reply fails with 502 upstream_failed. Gemini itself limits audio to 1 hour, or 30 minutes with word timestamps or speaker labels. Each request reserves the counted audio tokens plus 8,192 output tokens for up to 11 minutes.
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.
Realtime sessions (puter.ai.convo(), /puterai/openai/v1/realtime) require a paid plan, like the other OpenAI-compatible endpoints, and are capped at 20 open at once per account. An account without a plan gets an error event with code subscription_required. A session over the cap gets rate_limit_exceeded. Both close with 1008.
| Realtime transport limit | Value | Behavior at the limit |
|---|---|---|
| Client JSON message size | 64 KiB, including reassembled fragments | Close 1009. |
| Upstream JSON message size | 4 MiB, including reassembled fragments | Close 1011. A response that hasn't reported its usage is billed the credit held for it. |
| Fragments per client message | 64 | Connection closes. |
| Buffered receive chunks from the client | 128 | Connection closes. |
| Event type length | 1–128 characters | Invalid client envelopes receive invalid_event and close with 1008. Invalid upstream envelopes close with 1011. |
| Client message rate | 100/s, burst 100 | Error rate_limit_exceeded, close 1008. Ping and pong frames count toward this allowance. |
| Client upload rate | 128 KiB/s, burst 64 KiB | Reads pause to refill the budget. More than 64 KiB of overdraft closes with rate_limit_exceeded. |
| Input pending session setup | 32 messages or 64 KiB | Error rate_limit_exceeded, close 1008. |
| Forwarding queue in either direction | Reads pause at 128 KiB. Admission stops above 192 KiB toward the provider and above 4 MiB plus 128 KiB toward the client. | A full queue closes with rate_limit_exceeded. |
| Client that stops reading | 30 seconds of paused upstream reads | Close 1008. |
| Pending usage records | 32 | Error rate_limit_exceeded, close 1008. Records already received are still billed. |
| Numeric usage fields | Finite, nonnegative, at most 1 billion | Invalid upstream usage closes with 1011. A null usage counts as none. |
| Session setup timeout | 15 seconds | Error connection_timeout, close 1011. |
| Upstream handshake timeout | 10 seconds | Close 1011. |
| Heartbeat interval | 30 seconds | A missing pong terminates the connection on the next heartbeat. |
| Credential cache and recheck | Cache 60 seconds, sweep every 15 seconds, at most 16 checks in parallel | Invalid credentials close with 1008. Overlapping sweeps are skipped. |
| Session attempts per credential | 60 per minute | Error rate_limit_exceeded, close 1008. |
| Credit held per response | 8,000 audio input and 4,096 audio output tokens at the session model's rates, held until the response's usage is recorded | A session opens only if the account can cover one hold. A response the account can't cover gets insufficient_funds, close 1008, and is billed for what it generated. A response whose usage never arrives because the provider connection closed first is billed the full hold. |
Restart notice (puter.reconnect) |
At most once a minute per session | Repeated announcements within the minute aren't sent. |
| In-flight response drain at shutdown | 5 seconds | Close 1012. |
| Billing a response after the client disconnects | 5 seconds | The response is cancelled and billed for what it generated, including one that was requested but starts only after the disconnect. A response that doesn't report its usage within the 5 seconds is billed the credit held for it. |
Send audio as a stream of small chunks. The message cap applies to text, configuration, tool output, and audio alike.
The puter.ai.convo() SDK also applies these limits before sending or retaining data.
| Conversation SDK limit | Value | Behavior at the limit |
|---|---|---|
| Text per helper call | 8192 characters | Throws input_too_large. |
| Instructions | 8192 characters | Rejects with invalid_argument. |
| Voice identifier | 128 characters | Rejects with invalid_argument. |
| PCM chunk | 12000 samples at 24 kHz, or 500 ms | Throws input_too_large. |
| Native JSON traversal | Depth 16, 2048 visited values, 2048 array entries or object fields | Throws input_too_large before serialization. |
| Outbound buffered data | 128 KiB for audio, 192 KiB for other events | Throws queue_full. The built-in microphone drops the chunk instead. |
| Pending acknowledgments | 8 | Rejects with operation_in_progress. |
| Concurrent configuration updates | 1 | Rejects with operation_in_progress. |
| Authentication and provider acknowledgment timeout | 10 seconds | Rejects preparation or the pending operation. |
| Session creation timeout | 20 seconds | Rejects connect() with operation_timeout. |
| Browser media initialization timeout | 10 seconds, not counting the permission prompt | Rejects with media_timeout and releases resources. |
| Capture chunk duration | 20 ms | AudioWorklet sends 480 PCM16 samples per chunk. |
| Unacknowledged capture chunks | 25, or 500 ms | Further chunks are dropped until the page catches up. |
| Queued playback | 300 seconds | Further audio for the reply is dropped and queue_full is reported once. |
| Reconnection attempts | 10, backing off from 500 ms to 10 seconds, each delay randomized between half and one and a half times its value | The conversation closes with the original close code. |
| Replacement session after a restart notice | Opened within 3 seconds of the notice, then retried from 1 second, backing off to 10 seconds, each delay randomized between half and one and a half times its value. Settings on a held replacement are acknowledged within 3 seconds. | The current session keeps working until the conversation switches or the server closes it. A refusal that retrying can't fix stops the attempts. |
| Offline wait while reconnecting | 2 minutes per reconnection, in browsers that report being offline | Attempts continue after the wait. |
| Context restored after a reconnection | The 40 most recent restorable items, 48 KiB in total | Older items are left out. |
| Remembered canceled responses | 32 | Older response IDs are evicted from the suppression set. |
/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.
Chat requests accept JSON bodies up to 128 MiB (134,217,728 bytes), including text/plain;actually=json bodies. That covers puter-chat-completion calls through /drivers/call, which puter.ai.chat() uses, and POSTs to /puterai/openai/v1/*, /puterai/anthropic/v1/messages and /puterai/anthropic/v1/messages/count_tokens. A body over 50 MiB needs a Content-Length header and a token in the Authorization or x-api-key header (or a session cookie); an auth_token inside the body isn't read until the body is, so it only works up to 50 MiB. Larger bodies, and bodies over 50 MiB for other drivers, return HTTP 413. One account can have up to 128 MiB of these large bodies in flight at once; past that, requests get 429 with Retry-After. A server handling too many large bodies at once returns 503 with Retry-After. Retry either after the given number of seconds. Other routes accept JSON bodies up to 50 MiB.
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. |
| Kling | kling-v3-omni and kling-image-o1: 10 references; kling-v3 and kling-v2-1: one. Prompt up to 2,500 characters. Each reference is a JPEG or PNG up to 10 MB. Jobs are polled every 2 seconds for up to 5 minutes, then fail with 504 upstream_timeout and aren't charged. Each call to Kling times out after 60 seconds. |
| 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.
puter.ai.txt2vid() waits up to 14 minutes for a clip. These limits apply to Gemini Omni (gemini-omni-1.1-flash) on top of the AI limits above:
| Limit | Value |
|---|---|
| Prompt | 4,000 characters |
| Starting image | One PNG, JPEG or WebP image, 5 MiB. A URL is fetched with a 30-second timeout |
Source video (inputVideo) |
One MP4, QuickTime or WebM video, 32 MiB and 10 seconds, or 1 MiB as a data: URI. A URL is fetched with a 60-second timeout |
| Input tokens, prompt and image or video together | 131,072 |
| Requested output tokens | 65,536 |
| Input token count | 30-second timeout, 64 KiB reply |
| Generation | 14-minute timeout, reply and clip included. 64 MiB reply |
| Reservation | Counted input tokens, plus 173,760 video tokens and 65,536 text and thinking tokens, about $3.63 plus input. A source video isn't counted and adds 16,000 input tokens, about $0.02. Held for up to 15 minutes |
A longer prompt, a larger image or video, or input over the token limit fails with bad_request (413 input_too_large for an image over 5 MiB, a video over 32 MiB, or a data: URI video over 1 MiB) before generation starts. A source video over 10 seconds fails with 400 upstream_bad_request from Google. A reply over 64 MiB fails with 502 upstream_failed, and a generation that runs past 14 minutes fails with 504 upstream_timeout. Neither is charged.
The reservation is a conservative estimate based on verified generation usage, not a maximum charge. Google can return more output than the requested 65,536 tokens, and the charge is for the input, video, output and thinking tokens Google reports, even when that's more than the reservation. A reply that reports token usage is charged for it even when the reply failed or has no usable clip.
Kling video models take a prompt of up to 3,072 characters (Kling 3.0 models) or 2,500 (the others, and Kling 3.0 Turbo with input_reference). Kling 3.0 Omni and Kling O1 take a source video (inputVideo) as a URL or as a Puter file of up to 200 MiB, which fails with 413 input_too_large above that. Kling fetches either one itself. A Puter file is passed as a link that works for 19 minutes. The source must be an MP4 or QuickTime video of 3 to 15.5 seconds (10 on Kling O1). Kling rejects a source outside its limits, or one it can't decode, with upstream_bad_request, and the request isn't charged. An edit holds the price of a 16-second clip (10 on Kling O1). Each call to Kling times out after 60 seconds, and a job is polled every 5 seconds.
MiniMax video models take a prompt of up to 7,000 characters (H3 models) or 2,000 (Hailuo models), and up to 5 images as frames and references together (2 on H3 Max and Hailuo 02, 1 on Hailuo 2.3 and 2.3 Fast). Hailuo 2.3 Fast requires a first-frame image. Hailuo 02 supports first and last frames together at 768p or 1080p, and a first frame alone at 512p. MiniMax fetches an image URL itself, and a Puter file is sent inline. Each call to MiniMax times out after 60 seconds, and a job is polled every 5 seconds.
FLUX 3 on Together AI takes a source video (inputVideo) of up to 32 MiB as a Puter file, or up to 1 MiB as a data: URI. A larger one fails with 413 input_too_large before the job starts. A Puter file is passed to Together AI as a link that works for 19 minutes, and a URL is passed as is. Together AI fetches either one.
Image and video inputs to txt2img() and txt2vid() can name files in the user's Puter filesystem:
| Limit | Value |
|---|---|
| Image files per request | 32 |
| Image files per request, combined size | 32 MiB |
| Image file formats | PNG, JPEG, WebP or GIF |
| Source video file | See the model's source video limits above |
puter.js uploads a Blob, a File or a data: URI over 1 MiB to a temporary Puter file for the call. It counts against the user's storage until puter.js deletes it after the call.
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 |
Calls through share handles into one user's data for one app also spend a budget that every holder of those shares draws from, the same on every plan. All the holders together can spend 15,000 units per 10 seconds, and one holder at most 3,750 of them, so one busy holder can't use up the budget for the others. The budget counts storage use, so large entries and big listings spend more. A read spends about 1 unit for every 8 KB read, with each entry rounded up to 4 KB. A write spends 3 units for every 1 KB of each entry written. Every call spends at least 1 unit. So a get() of a small entry spends 1 unit, a set() of a value up to 1 KB spends 3, and a list() page that reads 1 MB spends 128. Past the holder's part or the total, calls through those shares fail with too_many_requests until older calls leave the 10-second window. The owner's own calls don't count, with or without a handle. How many shares an account can make and hold is listed under Events.
Sizes are the same for every account:
| Size | Limit |
|---|---|
| Key | 1 KB |
| Key, through a share handle | 768 bytes (UTF-8) |
list() pattern, through a share handle |
768 bytes (UTF-8) |
| Value | 399 KB (408,576 bytes) |
| 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 key you pass with a share handle is relative to the handle, and its 768-byte limit stays the same whatever the length of the shared prefix. An empty key or a longer one is refused with kv_handle_key_invalid, and so is a longer list() pattern.
A number out of range isn't 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 it's 5. With add(), the list it appends to counts as one more level. 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 |
| Thumbnail read URLs | Valid for at least 6 days and 23 hours, refreshed hourly | Per URL |
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 |
These limits cover file sharing through puter.fs. KV shares are listed under Key-value store.
| Limit | Value | Scope |
|---|---|---|
fs.share() / fs.unshare() calls |
60/min, 500/day | |
Reads (fs.getShares(), fs.listShared(), fs.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.
An app, its workers and the tokens it issues can send app.message notifications to the user they act for, and to the other side of a KV share made with notify: true. Each app has its own budget per recipient:
| Limit | Value | Scope |
|---|---|---|
app.message notifications |
30/min, 1,000/day | Per user, per app |
| Pushes | 12/min, 1,000/day | Per user, per app |
| Pushes from a new or unlisted app | 6/min, 200/day | Per user, per app |
| Notifications through a KV share | 30/min, 500/day | Per sender, recipient and app |
| Browsers with pushes turned on | 10 per app, 20 for Puter | Per user |
| Turning pushes on in a browser | 10/min | Per user |
| Confirmation pushes | 5/hour | Per browser |
title |
120 characters | |
text |
240 characters | |
icon (an https URL) |
2,048 characters | |
target (a path inside the app) |
512 characters | |
data |
1 KiB, measured as JSON | |
group |
128 characters | |
channel |
32 characters | |
ttl |
1 second to 1 day, default 1 hour |
Characters are counted as Unicode code points, so most emoji count as one. A notification's ttl is how long a push service keeps trying to reach an offline device, not how long the notification is kept. A value over any size limit, or with a field the format doesn't have, fails with 400 bad_request.
The notification limits count notifications written. A notification that replaces the app's earlier one with the same group counts against the per-minute limit only, not the per-day limit. notify() says when a notification replaces one. Over a limit, the call fails with 429 too_many_requests, and a refused call counts against neither window. A deployment can change these two limits with notificationAppMessageLimits: { perMinute, perDay }.
Notifications one side of a KV share sends the other through its handle count against the recipient's app.message limits, and also against the limit for that sender, recipient and app, across all their shares in that app. Over either one, the call fails with the same error, so the sender can't tell which limit it hit. A call that the recipient's limits refuse still counts against the share limit. The pushes count against the recipient's push limits and are billed to the recipient.
A notification goes out as a push only when no open page of the app answers within 5 seconds that it's visible and focused, or that it shows the notification itself. The push limits count only pushes sent, and a replacement that goes out as a push counts like any other. An app gets the lower push limits while it's younger than 30 days or not approved for listing. A website using Puter.js starts out unlisted, so it gets the lower limits. Over a push limit, the call still succeeds and the notification still reaches the app live, but it isn't pushed.
A push carries about 4 KB, and one that doesn't fit has its text shortened. Turning pushes on in another browser past an app's 10 removes that app's least recently seen one. A browser that goes 60 days without receiving a push or renewing its registration stops getting pushes. A page that calls onPush() renews it on load.
A push an app sends with puter.notifications.notify() is billed to the user who receives it:
| What | Rate | Counted per |
|---|---|---|
| A push from an app | 250 µ¢ ($2.50 per million) | Device that received the push |
A push to three of the user's devices counts three times. A push that didn't reach a device isn't charged. A user with no usage credit left gets no pushes from apps until they top up. Puter's own notifications, such as a share or a finished deploy, are free.
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. A seat beyond the owner's current seat limit is on org_seat_over_cap, which has no allowance: only purchased credit can be spent.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, Gaps for gap markers, and Suspended subscriptions 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 | 25,000 | 5,000 | 0 |
| KV share handles | Per user, per app | 10,000 | 2,000 | 0 |
| KV share handles with one other user | Per owner and holder | 256 | 256 | 0 |
| Session subscriptions | Per connection | 50 | 50 | 50 |
| Session subscriptions through one KV share handle | Per holder, all connections | 1,000 | 1,000 | 1,000 |
events_subscription_limit, and a share handle over its limit with events_kv_handle_limit_reached.events_durable_requires_account for persistent subscriptions and events_kv_handle_requires_account for share handles.puter.kv.listSharedByMe() for 30 days after they stop, and are then deleted.permission_revoked doesn't count toward the persistent subscription limits, since it never resumes. One suspended for any other reason still counts.puter.kv.share() call with several recipients is refused as a whole with events_kv_handle_limit_reached when its new shares would go over the all-apps or per-app limit. Sharing a prefix again with a user who already has it takes no slot. The limit per holder applies to each recipient on its own, and a recipient over it gets the error on their own item.app- and a uuid, the prefix without its trailing : can be at most 168 characters for watch, 163 for read, and 162 for write. In the store that belongs to no app, it can be at most 199, 194, and 193. A longer prefix is refused with invalid_kv_share_prefix.puter.perms.request('kvShare', …) names a prefix too. Its limit is 7 characters lower, so 161, 156, and 155 for an app uid of the usual form.kv_share_label_invalid.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 go out on the first 128 deliveries of a change that asked for them, in the order above and whatever the owner's plan. Later deliveries carry the key without the value, and so does every delivery 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 |
New recipients of kv.share() |
300/min, 2,000/hour | Per user, all apps |
Recipients named in kv.share() |
1,200/min | Per user, all apps |
kv.share.received notifications |
5/hour | Per owner and holder |
| 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 |
Calls over any of these fail with too_many_requests. A share over the kv.share.received limit is still made, without notifying the holder. Listing pages hold up to 200 items, and a fetch() page defaults to 50 events and holds up to 200.
A puter.kv.share() call names 1 to 50 recipients, and each one counts toward the named-recipient limit. A recipient who doesn't already have the share also counts toward the new-recipient limit, including a username that doesn't exist and a user the share is refused for. The call counts once toward the mint limit when it makes a share, changes one, or has any recipient who counts as new. A call where every recipient already has the share at the same access level and label counts only toward the named-recipient limit. When the recipients don't all fit in a limit, the whole call fails with too_many_requests, and the part that did fit still counts.
Unsubscribing counts toward its own unsubscribe limit, not the subscribe one. Changing a subscription's context with puter.events.update() counts toward the subscribe limit. Creating a schedule, or storing a notification with notify() and a sendAt more than 5 seconds ahead, counts toward the subscribe limit. Removing one counts toward unsubscribe, and listing or reading one toward the subscription listings.
| Limit | Value | Scope |
|---|---|---|
Deliveries without queue |
600/min | Per subscription |
queue deliveries |
120/min | Per subscription |
| Handler runs | 60/min | Per user, per app |
| Coalescing window | 250 ms | Per subscription and subject |
Time for a client to answer a queue delivery |
15 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).queue delivery waits and goes out later, and any other delivery 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 (queue, temporary errors) |
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 one resets the count to zero.
Only the handler's own failures count, including a run past the 30-second timeout. A run in the events worker that Puter couldn't start or finish, or that threw after a Puter API call from user got a 5xx, a 429, or a network error, is retried on the same delay schedule and doesn't count. When Puter's answer names a longer wait (Retry-After), the retry waits that long, up to 5 minutes. Puter's own failures never suspend a subscription, and the events it holds while retrying stay within the backlog limits above.
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. A handler can't start a chain outside this count through a worker of its own, since user.workers.create is refused.queue, it still reaches connected clients without running the persistent handler. A queued 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. A permission_revoked backlog is dropped right away. |
| Suspended subscription deleted after | 30 days |
Subscriptions suspended with permission_revoked kept |
500 per account. Past that, subscribing again deletes the oldest ones. |
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, 16 segments, one * per segment, and one ** in total. Through a KV share handle, the 256 characters are counted after the handle |
| 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 |
A pattern over its limits fails with invalid_subject_pattern, and a context over 4 KB with events_context_too_large.
These limits apply to onSchedule() schedules, and to notifications stored with notify() and sendAt. These are puter.com's limits.
| Limit | Scope | Paid | Free | Anonymous |
|---|---|---|---|---|
Recurring schedules (every and cron) |
Per user, per app | 100 | 10 | 0 |
| Recurring schedules | Per user, all apps | 1,000 | 50 | 0 |
at schedules and stored notifications waiting to go out |
Per user, per app | 1,000 | 100 | 0 |
at schedules and stored notifications waiting to go out |
Per user, all apps | 5,000 | 300 | 0 |
| Runs per hour of all recurring schedules added up | Per user, per app | 1,800 | 40 | 0 |
| Runs per hour of all recurring schedules added up | Per user, all apps | 18,000 | 120 | 0 |
A recurring schedule can run at most every 1 minute on a paid plan and every 15 minutes on a free one. For a cron schedule, that's the smallest gap between two of its runs.
events_schedule_limit, one over the runs per hour with events_schedule_budget, and one under the minimum interval with events_schedule_too_frequent. Anonymous accounts get events_durable_requires_account.every schedule counts 3,600 divided by its interval in seconds, rounded up to the next 0.001, so one that runs every minute counts 60, one that runs every 15 minutes counts 4, and one that runs every 7 minutes counts 8.572. A cron schedule counts the most runs it makes in any one hour, over its next 10,000 runs or 400 days. So a paid plan fits 30 schedules that run every minute in one app, and a free plan fits 10 that run every 15 minutes.at schedules and stored notifications count only toward the waiting limits, until they run or are cancelled.| Limit | Value | Scope |
|---|---|---|
| Schedule runs | 200 per minute | Per user, per app |
| Schedule runs at once | 8 per server | Per user, all apps |
| Time from a run's slot until it starts | Usually within 60 seconds | Per run |
| Spread after the slot, recurring | Under 30 seconds, or under a tenth of the smallest gap if that's less | Per schedule |
Spread after the slot, at and stored notifications |
Under 10 seconds | Per schedule |
| Run timeout | 30 seconds | Per run |
| Retry delay | 2 seconds, doubling, up to 5 minutes | Per run |
| Failures in a row before suspension | 5 | Per schedule |
| Wait while the user's account is suspended | 15 minutes, then tried again | Per run |
missed count |
Up to 1,000 | Per run |
maxRuns |
1 to 100,000 | Per schedule |
every, and how far ahead at, startAt, the first run and sendAt can be |
366 days | Per schedule |
| Cron expression | 128 characters | Per schedule |
Runs checked when a cron schedule is created |
10,000 runs or 400 days, whichever ends first | Per schedule |
cron schedules checked at once |
2, with 8 more waiting | Per server |
context |
4 KB | Per schedule |
| Events worker deploys started by schedule runs | 8 at once | Per region |
Ended schedules kept in listSchedules() |
7 days | Per schedule |
| A stored notification sent within 5 seconds | Sent right away instead | Per notification |
| Attempts to send a stored notification | 5, then it's dropped | Per notification |
| A stored notification over its recipient's limits | Dropped 6 hours after sendAt |
Per notification |
| A stored notification, as JSON | 16 KB | Per notification |
event.id and missed count.cron create that finds 2 checks running and 8 waiting fails with events_schedule_busy. Try again in a moment.at schedule's run, or a schedule's last run, is retried for 24 hours. A dropped run isn't billed and doesn't count as a failure.| Call | Limit | Scope |
|---|---|---|
onSchedule(), and notify() with a later sendAt |
60/min, shared with subscribe |
Per user, all apps |
removeSchedule() and notifications.cancel() |
600/min, shared with unsubscribe |
Per user, all apps |
listSchedules() and getSchedule() |
120/min, shared with the subscription listings | Per user, per app |
A listSchedules() page holds 50 schedules by default, and up to 200. Calls over any of these limits fail with too_many_requests. A create refused for its options, its handler or a missing permission still counts toward the onSchedule() limit, and removing a schedule that doesn't exist still counts toward the removeSchedule() limit.
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). Deliveries for queue subscriptions bill on the single line, and every other delivery, session subscriptions included, bills at the broadcast rate. Handler runs bill separately as worker usage.
Each schedule run bills once on the single line, to the user holding the schedule, however many attempts it takes. That includes a run whose handler threw. A schedule that runs every minute costs about $0.04 a month on top of its handler's worker usage. A stored notification bills nothing until it goes out, and then only its push, as What pushes cost describes.
Nothing is billed for idle subscriptions and schedules, skipped schedule runs (counted in missed), schedule runs stopped by the handler chain limit, 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, queue deliveries still waiting to reach a client or a handler, retries (a queue event bills once however many attempts it takes), and gap markers.
When the holder's balance runs out, deliveries stop and persistent subscriptions and schedules 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 token. Each token is valid for 24 hours, or until the session that created it ends. 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.