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
fetchbuffers a response whose known body size is at most 1 MiB. A larger or chunked body is streamed.fetchresolves when headers arrive, andresponse.bodyis aReadableStreamof Uint8Array chunks. It holds at most 1 MiB undelivered and pauses the transfer when full.text(),json(), andarrayBuffer()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.Responsehasstatus,ok,url,headers,body,text(),json(),arrayBuffer(), andbytes(). The last returns exact binary bytes for buffered or streamed bodies. A fetched response has noblob()orclone(). An unread body is cancelled when the call ends.- Returning
fetch(url)ornew 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
Responsebuilt on aReadableStreamfrom an HTTP route streams it to the client chunk by chunk. The stream can be one you created or an upstream body passed through aTransformStream. 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,WritableStreamandTransformStreamare full web streams with backpressure,tee,pipeTo,pipeThroughandfor await; no BYOB readers.- A request body with a Content-Length up to 1 MiB is buffered, and
request.bodyis its text or null. A larger or chunked upload is streamed and the call starts immediately. Itsrequest.bodyis aReadableStreamof 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(), andbytes()work on both forms and read at most 16 MiB. They throw an error above that limit; readrequest.bodyas 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 asReadableStream | string | null. fetchinit takesmethod,headers, a stringbodyand asignalonly. An abort cancels the transfer and rejects with the signal's reason (AbortError, orTimeoutErrorforAbortSignal.timeout).cryptohasgetRandomValues(typedArray)(at most 65536 bytes, as the spec says),randomUUID()and the full Web Cryptosubtle, on OpenSSL 3:digest(SHA-1, SHA-256, SHA-384, SHA-512);signandverifywith HMAC, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA (P-256, P-384, P-521, signaturer||s) and Ed25519;encryptanddecryptwith AES-GCM, AES-CBC, AES-CTR and RSA-OAEP;generateKey;importKeyandexportKeyinraw,pkcs8,spkiandjwk;deriveBitsandderiveKeywith PBKDF2, HKDF, ECDH and X25519;wrapKeyandunwrapKeywith 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. ACryptoKeyholds its key in native memory (secret bytes are cleansed when it is collected);algorithm,type,extractableandusagesare read-only properties. AES-CTR wraps only the lowlengthbits of its counter, as the spec defines.TextDecoderis UTF-8 only (another label throwsUnsupported encoding). Invalid UTF-8 becomes U+FFFD, indecode, inrequest.text()and in a fetchedresponse.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.structuredCloneis the standard algorithm (cycles, Map, Set, Date, RegExp, typed arrays, Errors; a function or Symbol throwsDataCloneError).queueMicrotaskruns on the engine's microtask queue, as in a browser.caches.defaultis 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 itsCache-Controls-maxageormax-age, else itsExpires;no-storeandprivateare not stored.deleteis supported.- CommonJS
moduleandexportsbelong to each function's wrapper; there is no globalmodule. setImmediatereturns an id andclearImmediatecancels it; the callback runs as a microtask.ctx.waitUntilpromises are awaited before the response is sent.env.<KV>andenv.<D1>are shims overdb.kvand SQL;D1.batchis not a transaction.dbis 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,deleteonly.Event(type)withpreventDefault()andstopPropagation();EventTargetwithaddEventListener,removeEventListener,dispatchEvent(no listener options).AbortControllerwithsignalandabort(reason?);AbortSignalwithaborted,reason,onabort,addEventListener/removeEventListener("abort"),throwIfAborted()and the staticsabort(reason?),timeout(ms)andany(signals).DOMException(message?, name?): an Error with a standard name (AbortError,TimeoutError,DataCloneError) andcode.performance.now(): milliseconds with sub-microsecond precision; the only member.Buffer: aUint8ArraywithBuffer.from(input, encoding?),alloc(size),concat(list, totalLength?),isBuffer(value)andtoString(encoding?); encodings are"hex","base64", else UTF-8; noreadUInt*,equalsorcopy.process.env(the same asenv),process.stdout.write(message)andprocess.stderr.write(message).setTimeout(fn, ms?, ...args)andsetInterval(fn, ms?, ...args)return a numeric id forclearTimeout(id)andclearInterval(id). Timers use the call's budget and are cleared when it ends.setImmediate,clearImmediate, andqueueMicrotaskare 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.