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 | the limit uses engine-reported heap capacity; over it the call can fail with 413 CF_MEMORY_EXCEEDED |
| 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 override the CPU, memory, concurrency, and wall-clock defaults above. Read the current values with tenant.usage.self.
App limits
Each app has storage, document, request-rate, and function limits. Read them with tenant.limits or tenant.usage.self, which also returns current usage alongside each limit. A limit is null until it is set.
{"name": "substrio_tenant_usage_self", "arguments": {}}
tenant.usage.history returns an app's call and storage history. It provides one point per completed minute, retained for two days, or per completed hour, retained for 90 days. Each point includes calls, status classes, latency buckets and percentiles, refusals, CPU and wall time, and the stored documents and bytes at the end of the interval.
Storage
An app's storage includes everything in its folder on disk. This figure counts against the app's storage limit and the account's space:
| Kind | What counts |
|---|---|
database |
The database file, including documents, indexes, KV, SQL tables, graph data, versions, and usage history. The file's used end is rounded up to a page. |
files |
Stored files, including uploads, site files, version sources, and tools. Each is counted once regardless of how it was uploaded. Its length is rounded up to a filesystem block (4 KiB on most disks), so a 1-byte file counts as 4 KiB. |
logs |
Function, delivery, and sync logs. Each is a database 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 returns the figure by kind, the limit, and the space remaining. account.storage returns figures for every app, whether open or closed, and the account total. The used_bytes and blob_bytes fields in tenant.usage.self and stored_bytes in account.overview continue to count document and file bytes alone.
{"name": "substrio_tenant_storage", "arguments": {}}
Storage measurements and quota enforcement work as follows:
- Measurements have different intervals. For each open app, the database's used end is measured once a second, and stored files are accounted for as they are written and deleted. Logs, backups, code, and other files are counted once a minute, so growth in those categories appears within a minute. When an app opens, its stored file blocks are counted again to correct any discrepancy caused by rounding.
- A write past the limit is refused with
507 STORAGE_QUOTA_EXCEEDEDbefore anything is stored. Between measurements, record and file bytes are charged to the quota. Database growth not charged to an individual write, including KV, SQL rows, indexes, and graph data, is reflected at the next one-second fold. Logs and other side files are reflected when they are recounted once a minute. These measurements can lag growth. Once the measured total exceeds the limit, the next write is refused. - The database file never shrinks. Space freed inside it by a delete is reused by later writes and is not returned to disk. A delete immediately returns its bytes to the quota, so writes that reuse the space are admitted. The measured figure changes only when the file grows. Deleting a backup or clearing a log lowers the figure at the next minute's count.
- A closed app reports the storage figure recorded when it closed.
| Refusal | HTTP | When |
|---|---|---|
STORAGE_QUOTA_EXCEEDED |
507 | The app's folder has reached or exceeded its storage limit; nothing was stored. |
SPACE_FULL |
507 | The account's apps together have reached or exceeded its space. Writes to its apps are refused until the total is below the limit. |
Key limits
An account or app key can have requests_per_minute and request_budget limits independent of the app-wide limit. Set them at creation with account.key.create or the corresponding arguments to tenant.key.rotate. Change them afterward with account.key.update or tenant.key.update. An omitted field keeps its current value, and 0 clears it.
request_budget is a lifetime count of admitted requests and does not refill. A key that exhausts its budget receives 429 KEY_BUDGET_EXHAUSTED until the budget is raised or the key is rotated. requests_per_minute is a rolling-minute rate. A request over that limit returns 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 returns 429 with Retry-After. rate_limit.list lists rules and their ids, and rate_limit.delete removes a rule by id. To change a rule, delete it and add a new one.
Plans
Your account's Substrio plan sets the limits of every app it owns. To move to a paid plan, use account.plan.checkout, which returns a checkout_url for payment. The plan does not change until payment is confirmed. account.plan.change moves an account to a free plan immediately if its usage fits that plan's limits. Otherwise, the call is refused and identifies the limit and the amount over it. account.plan.summary returns current usage alongside the current plan's allowances and lists the available plans and their additions.
billing.catalog lists what an account can buy. billing.cart.create prices a cart and returns a checkout_url, and billing.subscription.cancel ends a subscription immediately or at the end of the paid period. See /pricing for current prices.
Plans your app sells
Your app can define plans for its users' accounts. Each plan is a named set of limits and features. plans.set writes the app's plans file; an invalid file is refused and the current plans remain in force. plans.get reads the file. plans.account.set assigns a plan to a user's account, and plans.account.get returns its plan, limits, and features. An account without an assigned plan uses the default plan.