Limits apply at three scopes: a function call (fixed, per request), an app (set by its plan), and a key (optional, set when you issue it). Rate limits are rules you add yourself, on top of all three.
Per-call limits
Every function call runs under these bounds by default:
| Limit | Default | What happens over it |
|---|---|---|
| CPU time | 5,000 ms per call | 408; the isolate is discarded and the next call recompiles |
| JS heap | 256 MB per isolate | native memory (fetch bodies, SQL results, typed arrays) is not counted toward this |
| Wall clock | 30 s a call may wait on fetch and timers | the call fails with "The function's promise never settled" |
| Concurrency | 512 calls of one app in flight | a call over it gets 429; a full server queue gets 503 |
| Request body | 16 MiB for text(), json(), arrayBuffer() and bytes() |
a body over 1 MiB, or chunked, streams instead through request.body with backpressure |
| Response body | 64 MiB for a buffered response | the call fails with an error naming the limit; the body is never truncated |
| One value | 16 MiB for one KV value, one SQL parameter, or one fetch request or response body | the call throws an error naming the limit |
An app can run under its own overrides of the CPU, memory, concurrency and wall-clock defaults above; read your app's current values with tenant.usage.self.
App limits
An app has its own storage, document, request-rate and function limits, read with tenant.limits or tenant.usage.self, which answers the app's exact current usage alongside each limit. A limit is null until it is set.
{"name": "substrio_tenant_usage_self", "arguments": {}}
tenant.usage.history reads one app's call and storage history, one point per finished minute (kept two days) or hour (kept 90 days): calls, status classes, latency buckets and percentiles, refusals, CPU and wall time, and stored documents and bytes at the end of each point.
Storage
An app's storage is everything its folder holds on disk, and its storage limit, and its account's space, are held against that figure:
| Kind | What counts |
|---|---|
database |
the database file: documents, indexes, KV, SQL tables, graph, versions, usage history, everything the app keeps in it. Counted as the file's used end, rounded up to a page |
files |
every stored file (uploads, site files, version sources, tools), each once whatever uploads it, by the blocks it takes on disk: its length rounded up to the filesystem's block (4 KiB on most disks), so a 1-byte file counts 4 KiB |
logs |
the function and delivery logs and the sync log, each a database of its own counted at its used end |
backups |
the app's backups, and the copy a restore sets aside |
code |
functions, their bytecode cache, site releases and tools |
other |
every other file: the app's config and keys and the rest |
tenant.storage reads the figure by kind with the limit and the room left; account.storage reads every app's figure, open or not, and the account's total. tenant.usage's used_bytes and blob_bytes, and account.overview's stored_bytes, stay the document and file bytes alone.
{"name": "substrio_tenant_storage", "arguments": {}}
How it is kept, and what that means for you:
- It is current to the second. The figure is measured once a second for each open app (the database's used end, read with no system call; the stored files as they are written and deleted), and its logs, backups, code and other files are counted again once a minute, so a function's new bytecode cache, a log that grew or a backup shows within a minute. Every open of the app counts its stored files' blocks on the disk again, which corrects any block the rounding missed.
- A write past the limit is refused with
507 STORAGE_QUOTA_EXCEEDED, before anything is stored. Between two measurements a write is charged its own bytes; growth no write is charged for (KV, SQL rows, indexes, logs) is counted at the next measurement, so an app can pass its limit by at most one second of such growth. Its next write is then refused. - The database file never shrinks. Room a delete frees inside it is reused by later writes, never given back to the disk. A delete gives its bytes back to the limit at once, and writes that reuse that room are admitted; the figure itself only moves when the file grows. A backup deleted, or a log cleared, lowers the figure at the next minute's count.
- An app that is not open answers the figure it had when it closed: it holds nothing it did not hold then.
| Refusal | HTTP | When |
|---|---|---|
STORAGE_QUOTA_EXCEEDED |
507 | the app's folder holds its storage limit or more; nothing was stored |
SPACE_FULL |
507 | the account's apps together hold its space or more; every write of its apps is refused until the total is under the space |
Key limits
A key (account or app) can carry its own requests_per_minute and request_budget, independent of the app-wide limit. Set them at creation (account.key.create, and the requests_per_minute/request_budget arguments of tenant.key.rotate) or change them afterward with account.key.update or tenant.key.update; a field left out keeps its current value, and 0 clears it.
request_budget has no period: it is a lifetime count of admitted requests, never refilled. A key that spends its budget answers 429 KEY_BUDGET_EXHAUSTED until the budget is raised or the key is rotated. requests_per_minute is a rolling-minute rate; a key over it answers 429 KEY_RATE_LIMIT_EXCEEDED with Retry-After.
Rate limits
rate_limit.set adds a rule: at most max_requests per window_seconds on a set of operations or on the app's own function routes (a path pattern and method), counted per ip, token or tenant.
{"name": "substrio_rate_limit_set", "arguments": {"path": "/orders", "method": "POST", "by": "ip", "max_requests": 60, "window_seconds": 60}}
A request over a rule answers 429 with Retry-After. rate_limit.list lists every rule with its id; rate_limit.delete removes one by id. There is no update: delete the old rule, then add the new one.
Plans
Your account's Substrio plan sets the limits of every app it owns. An account moves to a paid plan through account.plan.checkout, which returns a checkout_url for the person to pay on; nothing changes until the payment is confirmed. account.plan.change moves an account to a free plan at once, when its usage already fits that plan's limits; otherwise it is refused, naming the limit and by how much the account is over. account.plan.summary reads what the current plan allows next to what the account uses, and every plan with what it adds, for an agent to explain the options from.
billing.catalog lists what an account can buy, billing.cart.create prices a cart and returns its own checkout_url, and billing.subscription.cancel ends a subscription (at the end of the paid period, or now). None of these calls print a price here: see /pricing for current figures.
Plans your app sells
Your app can define plans of its own for the accounts that use it, each a named set of limits and features. plans.set writes the app's plans file (a bad file is refused and the plans in force stay), and plans.get reads it. plans.account.set puts one of your users' accounts on a plan, and plans.account.get reads the plan it is on, with its limits and features; an account with no plan set is on the default plan.