OpenWorkers is a runtime for running javascript code in a serverless environment.
This runner manages instances of OpenWorkers Runtime.
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 wasmA 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.
| Backend | Feature | Code type | Snapshot / code cache | env | Bindings | Known limitations |
|---|---|---|---|---|---|---|
| V8 | v8 |
javascript, snapshot | yes | yes | all but images | no images handler on any backend; the only backend with an isolate pool, warm reuse and websockets |
| JSC | jsc |
javascript | no | yes | none | links the system JavaScriptCore; no websockets; a fresh context per request |
| QuickJS | quickjs |
javascript | no | no | none | no env, no websockets; a fresh runtime per request |
| Boa | boa |
javascript | no | no | none | no env, no websockets; a fresh context per request |
| WASM | wasm |
wasm | no | yes | kv, database, storage | wasi: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 not selectable: it wires no operations handler, so a guest has no
fetch, and nova_vm pulls a temporal_rs that does not build against the ICU
version v8 152 forces in a shared lockfile.
Optional on top of a backend: database (default), telemetry, and
multiplexing (v8 only, ignored elsewhere).
cargo run --features v8 --bin snapshotCREATE USER openworkers WITH PASSWORD 'password';
CREATE DATABASE openworkers WITH OWNER openworkers;DATABASE_URL='postgres://openworkers:password@localhost:5432/openworkers'
NATS_SERVERS='nats://localhost:4222'| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
NATS_SERVERS |
NATS server URL |
| Variable | Default | Description |
|---|---|---|
WORKER_DOMAINS |
workers.rocks |
Comma-separated list of worker domains for internal routing |
HTTP_POOL_MAX_IDLE_PER_HOST |
100 |
Max idle HTTP connections per host (for worker fetch()) |
Holds V8 code caches and precompiled wasm components, so a worker version is compiled once instead of on every cold start.
| Variable | Default | Description |
|---|---|---|
CODE_CACHE_MAX |
5000 |
Max entries in the in-memory LRU |
CODE_CACHE_MAX_BYTES |
536870912 |
Max 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.
| Variable | Default | Description |
|---|---|---|
V8_EXECUTE |
PINNED |
Execution mode: PINNED, POOLED, or ONESHOT |
WORKER_POOL_SIZE |
CPU cores | Number of V8 worker threads |
MAX_QUEUED_WORKERS |
pool × 10 | Max queued tasks before backpressure |
WORKER_WAIT_TIMEOUT_MS |
10000 |
Timeout (ms) waiting for a worker slot |
V8_EXECUTE modes:
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.
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.
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).
| Variable | Default | Description |
|---|---|---|
OTLP_ENDPOINT |
- | OTLP exporter endpoint (enables telemetry) |
OTLP_SERVICE_NAME |
openworkers-runner |
Service name reported to OTLP |
OTLP_HEADERS |
- | Extra headers for OTLP exporter |
| Variable | Default | Description |
|---|---|---|
NATS_CREDENTIALS |
- | Path to NATS credentials file |
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.localhostexport RUST_LOG=openworkers_runtime=debug,openworkers_runner=debug # Optional
cargo run --features v8cargo install sqlx-cli --no-default-features --features rustls,postgrescargo sqlx prepareWhen 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.13This 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.