The two Convex runtimes
A Convex function runs on one of two runtimes. Nimbus selects the runtime for
each module. A "use node"; directive on the first line selects the
Node-compatible runtime. Both runtimes use V8, with no separate Node process.
They expose different surfaces. The default runtime also applies determinism
rules that match Convex.
- Default runtime: a web-standard V8 isolate with
fetch,TextEncoder,crypto.subtle, and the other web globals, but no Node builtins. This is wherequery,mutation,httpAction, and directive-freeactionmodules run. - Node-compatible runtime: select this runtime with
"use node";at the top of a module. It runs node-compat on V8, not a spawned Node binary. It reaches Node builtins such asnode:cryptoandnode:async_hooks. A"use node"module may contain only actions. Put shared queries and mutations in a directive-free module. Call them withctx.runQueryorctx.runMutation.
The runtimes example app runs the same
computation on both and compares the results.
Determinism on the default runtime
Section titled “Determinism on the default runtime”Convex makes queries and mutations deterministic for replay and caching. Nimbus’s default runtime reproduces that behavior. It pins time and randomness for each invocation instead of reading a live clock or entropy source.
Randomness
Section titled “Randomness”Math.random() is a seeded pseudorandom number generator (PRNG). It uses a
ChaCha20 stream instead of the platform’s random source. Nimbus uses two seed
methods:
- At module load, Nimbus seeds it from the deployment stamp. A
module-scope
Math.random()value stays stable across runs and server restarts of the same deployment. - At each
query, paginated query, ormutationstart, Nimbus seeds it with fresh per-invocation entropy. Two calls in one handler differ. Each new handler starts with a fresh stream.
Nimbus freezes Date.now() and new Date() at each query, paginated query,
or mutation start. They stay constant for the whole handler, including
across await points. During module evaluation, they read the deployment
timestamp. Actions are exempt and see a live, side-channel-coarsened host
clock.
Nimbus fixes performance.now() during import and inside queries. It
increments inside mutations and actions. Nimbus pins performance.timeOrigin
to the deployment timestamp for every invocation kind.
Where the deployment timestamp comes from
Section titled “Where the deployment timestamp comes from”Nimbus derives the deployment stamp from the deployed bundle entrypoint’s modification time. A deployment rewrites that file. If Nimbus cannot read the modification time, it uses the first-observation time. The bundle SHA-256 seeds module-load randomness. Both values stay stable for a given deployed bundle. This stability keeps import-time values steady across restarts.
The modification time is a proxy, not a recorded deploy event. Copying or restoring a bundle can change the entrypoint modification time. Such a change shifts the deployment timestamp that functions observe.
It is parity, not a sandbox boundary
Section titled “It is parity, not a sandbox boundary”These rules reproduce Convex’s observable behavior. They are not a security control. A default-runtime function shares a trust domain with its own seed. Determinism provides behavioral parity, not isolation. Treat it as a compatibility feature, not a confinement feature.
fetch is available in actions only. Calling it from a query or mutation
fails closed with the same message Convex uses:
Can't use fetch() in queries and mutations. Please consider using an action.The tenant’s deny-by-default egress policy still applies to an action that passes this check. The query or mutation gate does not authorize a network destination. The egress policy decides that separately.
process.env
Section titled “process.env”The default runtime exposes only process.env on process. It does not expose
versions, release, cwd, or Buffer. It is not a Node environment. Reads
use the same capability-gated environment proxy as the Node lanes. Nimbus
denies access to variables without runtime grants instead of returning
undefined.
Async context
Section titled “Async context”The default runtime provides node:async_hooks, but only with
AsyncLocalStorage and AsyncResource. It also accepts the bare async_hooks
specifier. This is not the full Node surface. Nimbus does not provide
createHook or executionAsyncId. V8’s async-context frame backs the
implementation directly.
An AsyncLocalStorage store does not propagate into ctx.runQuery,
ctx.runMutation, or ctx.runAction. Those host calls run outside the
invocation’s async context. The called function cannot see a store that you set
with .run(...). Pass the value as an explicit argument.
WebAssembly
Section titled “WebAssembly”The WebAssembly API provides WebAssembly.instantiate,
WebAssembly.Module, and WebAssembly.Instance. Nimbus disables shared
WebAssembly memory to reduce side channels. A WebAssembly.Memory constructor
with shared: true throws.
The global surface is a superset
Section titled “The global surface is a superset”Nimbus’s default runtime adds several globals to the web-standard scope. They
include WebSocket, URL, structuredClone, and timer functions such as
setTimeout. This is a superset of the globals that Convex documents for
its default runtime. Code can behave differently if it detects a runtime by an
absent global. Code that uses only expected globals behaves the same.
Related pages
Section titled “Related pages”- Migrate a Convex app to Nimbus: what carries over and what to check first.
- Convex API compatibility: the full supported-surface matrix, including the runtime semantics summarized here.
- Convex example apps: runnable apps,
including the two-runtime
runtimesdemo.