openworkers/openworkers-runner

Rust

224

87 commits

updated Sep 14, 2026

See the code

README

OpenWorkers runner

OpenWorkers is a runtime for running javascript code in a serverless environment.

This runner manages instances of OpenWorkers Runtime.

Usage

Build

One JavaScript engine per build, and wasm on top of it if the runner should also serve components. Selecting no backend, or two JavaScript engines, is a compile error.

cargo build --release --features v8,wasm   # recommended for production
cargo build --release --features jsc
cargo build --release --features quickjs
cargo build --release --features boa
cargo build --release --features nova
cargo build --release --features wasm

A worker goes to the backend its code type names, so a build carrying both serves JavaScript workers and components from the same process.

Every backend serves fetch and, through Event::Task with a schedule source, scheduled. Bindings are supported per type: the runner refuses a worker that declares a binding its backend cannot serve, naming the types, rather than handing the guest an undefined env.ASSETS.

BackendFeatureCode typeSnapshot / code cacheenvBindingsKnown limitations
V8v8javascript, snapshotyesyesall but imagesno images handler on any backend; the only backend with an isolate pool, warm reuse and websockets
JSCjscjavascriptnoyesnonelinks the system JavaScriptCore; no websockets; a fresh context per request
QuickJSquickjsjavascriptnonononeno env, no websockets; a fresh runtime per request
Boaboajavascriptnonononeno env, no websockets; a fresh context per request
Novanovajavascriptnoyesassets, databasepure Rust, no C; no fetch(), no WebAssembly; crypto.subtle stops at HMAC and AES-GCM; a fresh agent per request
WASMwasmwasmnoyeskv, database, storagewasi:http/proxy components only; env arrives as WASI vars, not env; no assets or worker bindings

The wasm guest reaches its bindings through the openworkers:bindings WIT package rather than an env object: every call names its binding, and the runner resolves that name against the worker's bindings.

Nova is the one backend with no C in it: engine, parser and platform layer are all Rust, which is what makes it the candidate for a target where a V8 build is not worth its size. It scores 429 of the 448 tests openworkers-conformance measures, against v8's 448, and serves the dashboard.

Optional on top of a backend: database (default), telemetry, and multiplexing (v8 only, ignored elsewhere).

Snapshot the runtime (V8 only)

cargo run --features v8 --bin snapshot

Prepare the database

CREATE USER openworkers WITH PASSWORD 'password';
CREATE DATABASE openworkers WITH OWNER openworkers;

Create .env file

DATABASE_URL='postgres://openworkers:password@localhost:5432/openworkers'
NATS_SERVERS='nats://localhost:4222'

Environment Variables

Required

VariableDescription
DATABASE_URLPostgreSQL connection string
NATS_SERVERSNATS server URL

Networking

VariableDefaultDescription
WORKER_DOMAINSworkers.rocksComma-separated list of worker domains for internal routing
HTTP_POOL_MAX_IDLE_PER_HOST100Max idle HTTP connections per host (for worker fetch())

Code cache

Holds V8 code caches and precompiled wasm components, so a worker version is compiled once instead of on every cold start.

VariableDefaultDescription
CODE_CACHE_MAX5000Max entries in the in-memory LRU
CODE_CACHE_MAX_BYTES536870912Max total bytes in that LRU, whichever binds first

SNAPSHOT_CACHE_MAX and SNAPSHOT_CACHE_MAX_BYTES are still read when the CODE_CACHE_* name is unset, with a warning.

V8 Runtime

VariableDefaultDescription
V8_EXECUTEPINNEDExecution mode: PINNED, POOLED, or ONESHOT
WORKER_POOL_SIZECPU coresNumber of V8 worker threads
MAX_QUEUED_WORKERSpool × 10Max queued tasks before backpressure
WORKER_WAIT_TIMEOUT_MS10000Timeout (ms) waiting for a worker slot

V8_EXECUTE modes:

PINNED (default)

Thread-local isolate pools — each thread maintains its own pool of V8 isolates, keyed by tenant (user_id). Zero cross-thread contention. Multiple isolates can exist per tenant for concurrent requests. Includes backpressure via per-thread queue with configurable size and timeout.

A new V8 context is created per request, so no JS state leaks between requests. The isolate (engine, heap, GC) is reused to avoid the allocation cost.

POOLED

Single global LRU pool shared across all threads, protected by a mutex. Isolates are keyed by worker_id. Simpler model but higher contention under load since all threads compete for the same lock.

ONESHOT

Fresh V8 isolate per request, destroyed after each response. No reuse, no pooling. Slower (~1-2ms overhead per request) but useful for debugging. Also serves as a workaround for a V8 SIGSEGV (SEGV_PKUERR) that affects PINNED and POOLED modes in some containerized environments (see #2).

Telemetry (OpenTelemetry)

VariableDefaultDescription
OTLP_ENDPOINT-OTLP exporter endpoint (enables telemetry)
OTLP_SERVICE_NAMEopenworkers-runnerService name reported to OTLP
OTLP_HEADERS-Extra headers for OTLP exporter

NATS Authentication

VariableDefaultDescription
NATS_CREDENTIALS-Path to NATS credentials file

Internal Routing (WORKER_DOMAINS)

When a worker calls fetch() to a URL matching *.{domain}, the request is routed internally instead of going through DNS and external network. This improves latency and avoids external bandwidth costs.

// These are routed internally (no DNS lookup):
fetch("https://my-api.workers.rocks/endpoint");

// This goes through external network:
fetch("https://example.com/api");

Configure for your environment:

# Production (default)
WORKER_DOMAINS=workers.rocks

# Local development
WORKER_DOMAINS=workers.dev.localhost

# Both
WORKER_DOMAINS=workers.rocks,workers.dev.localhost

Run

export RUST_LOG=openworkers_runtime=debug,openworkers_runner=debug # Optional

cargo run --features v8

Install sqlx-cli (optional - only for development)

cargo install sqlx-cli --no-default-features --features rustls,postgres

Prepare the database

cargo sqlx prepare

Known Issues

temporal_rs build failure with Deno runtime

When building with the deno feature (default), you may encounter a build error with temporal_rs:

error: unexpected end of macro invocation
  --> temporal_rs-0.0.11/src/tzdb.rs:60:1
   |
60 | timezone_provider::iana_normalizer_singleton!();
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing tokens in macro arguments

Workaround: Pin timezone_provider to version 0.0.13:

cargo update -p timezone_provider@0.0.16 --precise 0.0.13

This is a known upstream issue with temporal_rs and newer versions of timezone_provider. The Cargo.lock file should preserve this fix for subsequent builds.

Contributors

max-lt

87 commits

openworkers/openworkers-runner

Rust

224

87 commits

updated Sep 14, 2026

See the code

README

OpenWorkers runner

OpenWorkers is a runtime for running javascript code in a serverless environment.

This runner manages instances of OpenWorkers Runtime.

Usage

Build

One JavaScript engine per build, and wasm on top of it if the runner should also serve components. Selecting no backend, or two JavaScript engines, is a compile error.

cargo build --release --features v8,wasm   # recommended for production
cargo build --release --features jsc
cargo build --release --features quickjs
cargo build --release --features boa
cargo build --release --features nova
cargo build --release --features wasm

A worker goes to the backend its code type names, so a build carrying both serves JavaScript workers and components from the same process.

Every backend serves fetch and, through Event::Task with a schedule source, scheduled. Bindings are supported per type: the runner refuses a worker that declares a binding its backend cannot serve, naming the types, rather than handing the guest an undefined env.ASSETS.

BackendFeatureCode typeSnapshot / code cacheenvBindingsKnown limitations
V8v8javascript, snapshotyesyesall but imagesno images handler on any backend; the only backend with an isolate pool, warm reuse and websockets
JSCjscjavascriptnoyesnonelinks the system JavaScriptCore; no websockets; a fresh context per request
QuickJSquickjsjavascriptnonononeno env, no websockets; a fresh runtime per request
Boaboajavascriptnonononeno env, no websockets; a fresh context per request
Novanovajavascriptnoyesassets, databasepure Rust, no C; no fetch(), no WebAssembly; crypto.subtle stops at HMAC and AES-GCM; a fresh agent per request
WASMwasmwasmnoyeskv, database, storagewasi:http/proxy components only; env arrives as WASI vars, not env; no assets or worker bindings

The wasm guest reaches its bindings through the openworkers:bindings WIT package rather than an env object: every call names its binding, and the runner resolves that name against the worker's bindings.

Nova is the one backend with no C in it: engine, parser and platform layer are all Rust, which is what makes it the candidate for a target where a V8 build is not worth its size. It scores 429 of the 448 tests openworkers-conformance measures, against v8's 448, and serves the dashboard.

Optional on top of a backend: database (default), telemetry, and multiplexing (v8 only, ignored elsewhere).

Snapshot the runtime (V8 only)

cargo run --features v8 --bin snapshot

Prepare the database

CREATE USER openworkers WITH PASSWORD 'password';
CREATE DATABASE openworkers WITH OWNER openworkers;

Create .env file

DATABASE_URL='postgres://openworkers:password@localhost:5432/openworkers'
NATS_SERVERS='nats://localhost:4222'

Environment Variables

Required

VariableDescription
DATABASE_URLPostgreSQL connection string
NATS_SERVERSNATS server URL

Networking

VariableDefaultDescription
WORKER_DOMAINSworkers.rocksComma-separated list of worker domains for internal routing
HTTP_POOL_MAX_IDLE_PER_HOST100Max idle HTTP connections per host (for worker fetch())

Code cache

Holds V8 code caches and precompiled wasm components, so a worker version is compiled once instead of on every cold start.

VariableDefaultDescription
CODE_CACHE_MAX5000Max entries in the in-memory LRU
CODE_CACHE_MAX_BYTES536870912Max total bytes in that LRU, whichever binds first

SNAPSHOT_CACHE_MAX and SNAPSHOT_CACHE_MAX_BYTES are still read when the CODE_CACHE_* name is unset, with a warning.

V8 Runtime

VariableDefaultDescription
V8_EXECUTEPINNEDExecution mode: PINNED, POOLED, or ONESHOT
WORKER_POOL_SIZECPU coresNumber of V8 worker threads
MAX_QUEUED_WORKERSpool × 10Max queued tasks before backpressure
WORKER_WAIT_TIMEOUT_MS10000Timeout (ms) waiting for a worker slot

V8_EXECUTE modes:

PINNED (default)

Thread-local isolate pools — each thread maintains its own pool of V8 isolates, keyed by tenant (user_id). Zero cross-thread contention. Multiple isolates can exist per tenant for concurrent requests. Includes backpressure via per-thread queue with configurable size and timeout.

A new V8 context is created per request, so no JS state leaks between requests. The isolate (engine, heap, GC) is reused to avoid the allocation cost.

POOLED

Single global LRU pool shared across all threads, protected by a mutex. Isolates are keyed by worker_id. Simpler model but higher contention under load since all threads compete for the same lock.

ONESHOT

Fresh V8 isolate per request, destroyed after each response. No reuse, no pooling. Slower (~1-2ms overhead per request) but useful for debugging. Also serves as a workaround for a V8 SIGSEGV (SEGV_PKUERR) that affects PINNED and POOLED modes in some containerized environments (see #2).

Telemetry (OpenTelemetry)

VariableDefaultDescription
OTLP_ENDPOINT-OTLP exporter endpoint (enables telemetry)
OTLP_SERVICE_NAMEopenworkers-runnerService name reported to OTLP
OTLP_HEADERS-Extra headers for OTLP exporter

NATS Authentication

VariableDefaultDescription
NATS_CREDENTIALS-Path to NATS credentials file

Internal Routing (WORKER_DOMAINS)

When a worker calls fetch() to a URL matching *.{domain}, the request is routed internally instead of going through DNS and external network. This improves latency and avoids external bandwidth costs.

// These are routed internally (no DNS lookup):
fetch("https://my-api.workers.rocks/endpoint");

// This goes through external network:
fetch("https://example.com/api");

Configure for your environment:

# Production (default)
WORKER_DOMAINS=workers.rocks

# Local development
WORKER_DOMAINS=workers.dev.localhost

# Both
WORKER_DOMAINS=workers.rocks,workers.dev.localhost

Run

export RUST_LOG=openworkers_runtime=debug,openworkers_runner=debug # Optional

cargo run --features v8

Install sqlx-cli (optional - only for development)

cargo install sqlx-cli --no-default-features --features rustls,postgres

Prepare the database

cargo sqlx prepare

Known Issues

temporal_rs build failure with Deno runtime

When building with the deno feature (default), you may encounter a build error with temporal_rs:

error: unexpected end of macro invocation
  --> temporal_rs-0.0.11/src/tzdb.rs:60:1
   |
60 | timezone_provider::iana_normalizer_singleton!();
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing tokens in macro arguments

Workaround: Pin timezone_provider to version 0.0.13:

cargo update -p timezone_provider@0.0.16 --precise 0.0.13

This is a known upstream issue with temporal_rs and newer versions of timezone_provider. The Cargo.lock file should preserve this fix for subsequent builds.

Contributors

max-lt

87 commits

Languages

Rust

99.6%