Plans and limits

Read what bounds one function call, one app and one key, and how an account's plan sets those bounds.

Docs menu

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.