Plans and limits

Limits apply to function calls, apps, and keys. An account's plan sets app limits, and you can add rate-limit rules.

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 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_EXCEEDED before 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.