Function runtime

JavaScript functions have database, platform service, environment, and fetch globals in scope.

Docs menu

These globals are db, substrio, env, and fetch. Each call runs within the configured platform limits.

The full, typed signature of every global is at /docs/runtime/api/ (served live at GET /runtime.d.ts, over MCP as the resource substrio://runtime/types). These pages explain it; the types are the source of truth.

  • kv.md: key-value stores of bytes.
  • sql.md: db.sql, db.prepare, parameters, db.transaction.
  • collections-and-chains.md: documents and the chain.
  • chain-reference.md: every chain method with its arguments, answer, errors and an example; the filter operators; how a chain runs.
  • records.md: live records and query results.
  • services.md: the substrio.* services.
  • durable-objects.md: exported classes, one live instance per id, storage, alarms.
  • queues.md: durable queues and background jobs, substrio.queue.
  • errors-and-logs.md: what a failure records and how to read it.
  • rust.md: Rust functions, when to pick one, every call exposed to it, how to deploy and test it.
  • wasm-functions.md: functions in Rust compiled to WebAssembly, with the same request, response, limits and counters.

A function

Use one file per function; the loader has no resolver for other files. A function that imports only the builtin modules is sent as written. The CLI bundles other imports before deployment with substrio deploy or substrio push; the server does not bundle them. See cloud-functions.md. The runtime recognizes three forms:

// ES module
export default { async fetch(request, env, ctx) { return Response.json({ ok: true }); } };

// CommonJS
module.exports = async function (event) { return { ok: true }; };

// Script: evaluates to a function
(function (event) { return { ok: true }; })

The server resolves builtin modules without a bundle. The CLI resolves npm packages from the project's node_modules at bundle time.

Handler shapes

Each trigger kind has one entry. Cron, document and queue triggers exist; there is no email trigger, so an email() export is never called.

Call Entry
HTTP route fetch(request, env, ctx), or the default-export function
cron scheduled(event, env, ctx), else the default-export function called as handler(event)
queue delivery queue(batch, env, ctx) (see queues.md)
document trigger the default-export function, handler(event)
substrio.functions.<name>(...), plain call the default-export function, handler(...args)
substrio.functions.<name>.fetch(request) fetch(request, env, ctx)

export default { fetch } is not called for anything but an HTTP route call. A cron or trigger call on a module without the export it needs fails, and the error names the export (for cron: "a cron trigger needs a default-export function or a scheduled(event, env, ctx) export").

export default {
  async scheduled(event, env, ctx) {          // event.cron, event.scheduledTime (ms)
    db.counters.insert({ at: event.scheduledTime });
  },
};

A handler may be async. Everything in db is synchronous.

When a run fails

A cron run, document trigger run, or queue delivery fails when the handler throws, traps, hits a limit, or returns a status of 400 or more, such as new Response(text, { status: 500 }). Failed runs follow their retry policies: cron uses backoff and cron.max_attempts, triggers use triggers.max_attempts, and queues redeliver messages under max_retries before dead-lettering them. See delivery.md and queues.md. The response body becomes the run's error. Direct calls retain their response status, including HTTP routes, /fn/<name>, function.invoke, and calls from one function to another. Rust functions fail in the same cases; see wasm-functions.md.

Limits

Limit Value
CPU time per call 5,000 ms; over it: HTTP 408 CF_CPU_TIMEOUT
JS heap per isolate 256 MB of engine-reported heap capacity; over it the call can fail with HTTP 413 CF_MEMORY_EXCEEDED
Memory across all of the app's isolates over it, a call that needs a new isolate gets HTTP 503 CF_TENANT_MEMORY, retryable
Wall clock per call 30 s; over it: "The function's promise never settled"
Concurrent calls of one app 512 in flight; over it: HTTP 429
Server busy HTTP 503, retryable
Cron and trigger runs when the server is busy refused: a cron run is recorded as refused and retried with backoff (cron.max_attempts), then dead-lettered
Request body 16 MiB
Response body 64 MiB; over it the call fails with an error naming the limit
One kv value, SQL parameter, fetch request or response body 16 MiB; over it the call throws an error naming the limit
Direct call nesting (substrio.functions) 64 deep; the next call throws call_depth_exceeded naming the limit
fetch 30 s per request; loopback and private addresses refused

An app can have CPU, memory, concurrency, and wall-clock limits that differ from the defaults. tenant.usage.self returns the limits in force. No app is unlimited. A run's timeout_ms and memory_mb override the app's values.

Nothing is truncated: a value over a limit is an error, never a cut. The message is the size and the limit in bytes, with the place in front when the call has one:

Over the limit Error message
Response body (64 MiB) response body of 70000000 bytes exceeds the limit of 67108864 bytes (HTTP 413)
A SQL parameter SQL param 2: single value of N bytes exceeds the limit of 16777216 bytes
A fetch request body fetch request single value of N bytes exceeds the limit of 16777216 bytes
A fetch response body read in full fetch response single value of N bytes exceeds the limit of 16777216 bytes
A kv value kv.put: single value of N bytes exceeds the limit of 16777216 bytes

State between calls

Your app runs in several isolates at once. Each keeps its global context between calls, so globalThis, module-level variables, caches, and compiled code persist within an isolate and differ between isolates. An isolate can be replaced between calls, for example after a limit ends a call, when its heap stays high, or after it has had no calls for 10 minutes (jsc.idle_drop_ms). Treat module state as a cache that may be empty on any call. Persist data in db, KV, or records.

Differences from Workers

  • fetch buffers a response whose known body size is at most 1 MiB. A larger or chunked body is streamed. fetch resolves when headers arrive, and response.body is a ReadableStream of Uint8Array chunks. It holds at most 1 MiB undelivered and pauses the transfer when full. text(), json(), and arrayBuffer() read up to 16 MiB and throw an error above that limit. Read the stream to go beyond it. A body can be read once. Response has status, ok, url, headers, body, text(), json(), arrayBuffer(), and bytes(). The last returns exact binary bytes for buffered or streamed bodies. A fetched response has no blob() or clone(). An unread body is cancelled when the call ends.
  • Returning fetch(url) or new Response(upstream.body, init) with the body untouched sends upstream bytes to the client as they arrive, without reading them in JavaScript. Memory stays near 1 MiB regardless of body size. A client disconnect cancels the upstream transfer. If the upstream fails mid-body, the client connection closes and one log line is written.
  • Returning a Response built on a ReadableStream from an HTTP route streams it to the client chunk by chunk. The stream can be one you created or an upstream body passed through a TransformStream. The call stays active, subject to wall and CPU limits. The stream is pulled only as fast as the client reads, with at most 1 MiB undelivered. A client disconnect cancels the reader, and a stream error closes the connection. A body you read and return as text or bytes is a buffered response with a 64 MiB limit; a streamed response has no size limit. A stream that sends no chunk for 30 s ends. The limit for receiving fetch headers remains 30 s. Non-HTTP callers, including cron, triggers, and MCP, receive the whole body up to 64 MiB; larger bodies produce an error naming the limit.
  • ReadableStream, WritableStream and TransformStream are full web streams with backpressure, tee, pipeTo, pipeThrough and for await; no BYOB readers.
  • A request body with a Content-Length up to 1 MiB is buffered, and request.body is its text or null. A larger or chunked upload is streamed and the call starts immediately. Its request.body is a ReadableStream of Uint8Array chunks that can be read once. The stream holds at most 1 MiB undelivered. When it is full, the server stops reading the socket until the function catches up. A streamed upload has no 16 MiB total limit, but the wall-time limit and a 30 s idle rule apply. An upload that stalls for 30 s or a client disconnect causes a reader error. request.text(), json(), arrayBuffer(), and bytes() work on both forms and read at most 16 MiB. They throw an error above that limit; read request.body as a stream to go beyond it. If a function returns without reading the upload, its response is sent. The rest of a small upload, up to 1 MiB, is drained so the connection can be reused. For a larger upload, the connection closes after the response (Connection: close). Type the body as ReadableStream | string | null.
  • fetch init takes method, headers, a string body and a signal only. An abort cancels the transfer and rejects with the signal's reason (AbortError, or TimeoutError for AbortSignal.timeout).
  • crypto has getRandomValues(typedArray) (at most 65536 bytes, as the spec says), randomUUID() and the full Web Crypto subtle, on OpenSSL 3: digest (SHA-1, SHA-256, SHA-384, SHA-512); sign and verify with HMAC, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA (P-256, P-384, P-521, signature r||s) and Ed25519; encrypt and decrypt with AES-GCM, AES-CBC, AES-CTR and RSA-OAEP; generateKey; importKey and exportKey in raw, pkcs8, spki and jwk; deriveBits and deriveKey with PBKDF2, HKDF, ECDH and X25519; wrapKey and unwrapKey with AES-GCM, AES-CBC, AES-CTR, AES-KW and RSA-OAEP. The calls answer already settled promises and reject with an Error named as Web Crypto names it (TypeError, NotSupportedError, InvalidAccessError, DataError, OperationError, SyntaxError). The work is synchronous native CPU inside the call and counts against its CPU budget: generating a 2048 bit RSA key takes tens to hundreds of milliseconds, the other operations microseconds to a millisecond. A CryptoKey holds its key in native memory (secret bytes are cleansed when it is collected); algorithm, type, extractable and usages are read-only properties. AES-CTR wraps only the low length bits of its counter, as the spec defines.
  • TextDecoder is UTF-8 only (another label throws Unsupported encoding). Invalid UTF-8 becomes U+FFFD, in decode, in request.text() and in a fetched response.text(); NUL bytes and the bytes of a subarray are kept. new TextDecoder("utf-8", { fatal: true }) throws an Error (The encoded data was not valid for encoding utf-8) instead. structuredClone is the standard algorithm (cycles, Map, Set, Date, RegExp, typed arrays, Errors; a function or Symbol throws DataCloneError).
  • queueMicrotask runs on the engine's microtask queue, as in a browser.
  • caches.default is best effort: per isolate (not shared between worker threads, gone when the isolate is retired), at most 16 MiB with least-recently-used eviction. An entry expires at its Cache-Control s-maxage or max-age, else its Expires; no-store and private are not stored. delete is supported.
  • CommonJS module and exports belong to each function's wrapper; there is no global module.
  • setImmediate returns an id and clearImmediate cancels it; the callback runs as a microtask.
  • ctx.waitUntil promises are awaited before the response is sent.
  • env.<KV> and env.<D1> are shims over db.kv and SQL; D1.batch is not a transaction.
  • db is synchronous and in process; there is no network hop to the database.

Globals

Everything a function can call is typed in GET /runtime.d.ts. Beyond the pages above:

  • URL(url, base?): href, protocol, host, hostname, port, pathname, search, hash, origin, searchParams, toString().
  • URLSearchParams(init?): get, getAll, has, set, append, delete, forEach, entries, keys, values, toString.
  • atob(data), btoa(data): base64 of a string.
  • Blob(parts?, { type? }): text(), arrayBuffer(), slice(start?, end?, type?).
  • FormData: append, get, has, delete only.
  • Event(type) with preventDefault() and stopPropagation(); EventTarget with addEventListener, removeEventListener, dispatchEvent (no listener options).
  • AbortController with signal and abort(reason?); AbortSignal with aborted, reason, onabort, addEventListener/removeEventListener("abort"), throwIfAborted() and the statics abort(reason?), timeout(ms) and any(signals).
  • DOMException(message?, name?): an Error with a standard name (AbortError, TimeoutError, DataCloneError) and code.
  • performance.now(): milliseconds with sub-microsecond precision; the only member.
  • Buffer: a Uint8Array with Buffer.from(input, encoding?), alloc(size), concat(list, totalLength?), isBuffer(value) and toString(encoding?); encodings are "hex", "base64", else UTF-8; no readUInt*, equals or copy.
  • process.env (the same as env), process.stdout.write(message) and process.stderr.write(message).
  • setTimeout(fn, ms?, ...args) and setInterval(fn, ms?, ...args) return a numeric id for clearTimeout(id) and clearInterval(id). Timers use the call's budget and are cleared when it ends. setImmediate, clearImmediate, and queueMicrotask are described above.

Builtin modules

A function may import or require only the modules below. Another specifier fails when an import loads the function or, for require, with code: "MODULE_NOT_FOUND". The loader parses an ES module to determine its imports. Each module loads once per isolate context on first use, and import and require provide the same exports. Dynamic import() is not available.

Import the per-function env in an ES module. import { env } from "substrio:env" provides the function's env in every continuation. Without that import, a module reading the bare global env sees the env of whichever function the isolate ran last. After an await or a direct call, that can be another function's env. A CommonJS function and a script consisting of one function expression receive their env as a parameter and need no import.

Specifier (the node: form and the bare Node name both work) What it is
substrio:env env: in an ES module, the importing function's own env (each function that imports it gets its own instance), so it stays that function's env after an await and during direct calls; through require, the running call's env. An env is a plain object of read-only values (a write or a delete throws in strict code and changes nothing otherwise; nothing can be added), updated in place when the function's secrets, vars or bindings change, so env.X is one property read. env.ASSETS is the function's static assets (not enumerated)
node:buffer Buffer, Blob, atob, btoa, INSPECT_MAX_BYTES (the runtime's own Buffer)
node:crypto createHash, createHmac, hash, randomBytes, randomFillSync, randomInt, randomUUID, getRandomValues, timingSafeEqual, createSign, createVerify, sign, verify (PEM keys: PKCS#8, SPKI, PKCS#1, SEC1; padding: constants.RSA_PKCS1_PSS_PADDING; dsaEncoding: "ieee-p1363"), pbkdf2, pbkdf2Sync, getHashes, constants, webcrypto, subtle
node:events EventEmitter (and once, getEventListeners, setMaxListeners, errorMonitor)
node:util format, inspect (the common cases), promisify, callbackify, inherits, deprecate, isDeepStrictEqual, types, TextEncoder, TextDecoder
node:url URL, URLSearchParams, legacy parse, format, resolve, fileURLToPath, pathToFileURL, urlToHttpOptions, domainToASCII
node:path, node:path/posix the POSIX functions; resolve has no working directory and uses /
node:querystring parse, stringify, escape, unescape
node:string_decoder StringDecoder for utf8, utf16le, latin1, ascii, hex, base64 and base64url
node:assert, node:assert/strict assert, ok, equal, strictEqual, deepEqual, deepStrictEqual, their not forms, throws, doesNotThrow, rejects, doesNotReject, match, ifError, fail, AssertionError

node:crypto hashes are SHA-1, SHA-256, SHA-384 and SHA-512 (also spelled RSA-SHA256, sha-256, ...); another digest such as MD5 throws Digest method not supported. Its signing, hashing and HMAC run on the same native core as crypto.subtle. createSign and createVerify take PEM keys, not KeyObjects (there is no createPrivateKey or generateKeyPair; use crypto.subtle for those). PSS signs with the maximum salt and verifies any salt.

require is a read-only, non-deletable global function that your code cannot replace; a CommonJS function and an ES module see the same one. require.cache is an array of the loaded modules' exports, indexed by module.