← Open Tech Foundation Blog

Introducing ES Runtime: a secure, standards-based server runtime

A V8-based, WinterTC-compliant ECMAScript runtime with a Rust core, a deny-by-default capability model, and a separate development toolchain binary.

AI-assisted writing

This post was written with the help of AI. The ideas, decisions, and final review are our own.

Today we are introducing ES Runtime (esrun), a server-side ECMAScript runtime from the Open Tech Foundation.

ES Runtime is built on V8 with a Rust core. It conforms to WinterTC, it loads ES modules only, and a capability model governs everything it can touch: a program may do nothing that the command line does not grant.

ES Runtime is in alpha (v0.45.0) and is licensed under Apache-2.0.

Why another runtime?

Server-side JavaScript has a security problem that its runtimes were not designed to solve. A typical service pulls in hundreds of packages, and each one runs with the full authority of the process: it can read any file, open any socket, and spawn any program. The service's actual reach is whatever its least careful transitive dependency decides it is.

ES Runtime starts from the other end. It asks what a service needs to do, grants exactly that, and refuses everything else at the boundary between JavaScript and the host. The rest of the design follows from that decision:

  • Standards first. WinterTC APIs such as Request, Response, fetch and Web Crypto, with ESM as the only module format.

  • Secure by construction. A capability-based permission model that denies everything by default.

  • A focused production binary. esrun runs services and nothing else. Development tooling lives in a separate binary, esdev.

  • Fewer dependencies. The infrastructure most services need ships inside the runtime instead of arriving through a dependency tree.

A minimal example

JavaScript
import { serve } from "runtime:http";

const server = serve({ port: 8080 }, (req) => {
  return new Response("Hello from ES Runtime!");
});

const { hostname, port } = await server.addr;
console.log(`listening on http://${hostname}:${port}`);
Shell
esrun --allow-listen app.js

This server may accept connections, and that is all. It cannot read a file, make an outbound request, or read an environment variable, because none of those were granted.

Security model

Security is the central design constraint of ES Runtime, not a feature added on top. In short:

  • Deny by default. A run can reach only what its command line grants, across eleven capabilities such as read, write, net and env. Checks happen at every host operation, not at import time, so a transitive dependency cannot route around them.

  • A filesystem root jail with no escape switch. Every file and module path is confined to the working directory, symlinks and .. included. No flag turns the jail off, and guest code can never widen it.

  • Explicit read-only access. Read and write are separate grants, and --allow-read never makes a path writable.

  • The deploy line is the policy. The command line is the complete statement of what a service may do, with no separate policy file to drift out of sync.

  • A small production surface. esrun has no watcher, debugger port or transform step. FFI, native addons, remote imports and runtime CommonJS are left out on purpose.

Shell
esrun --allow-listen=443 \
      --allow-read=/etc/letsencrypt/live/example.com \
      --allow-write=./logs \
      server.js

This service may accept connections on port 443, read its TLS certificates, and write only to ./logs.

Read the full model, with its guarantees and its known limits, in the official security guide.

esdev: the development toolchain

esdev runs the same runtime and adds what you need to build, test and iterate on a service. It is meant for development machines and CI, not production.

The permission flags are the same in both binaries, with one deliberate difference: esdev grants everything by default, while esrun grants nothing. You should not need flags to run the program you are in the middle of writing, but a deployment should state exactly what it may reach. To develop under production's grant instead, pass --deny-all and add the capabilities you want.

AreaWhat esdev provides
LanguagesTypeScript and JSX, with CommonJS packages converted at build time
BuildA bundler with CSS and Tailwind support
TestingA built-in test runner with DOM parity, and real-browser runs over WebDriver BiDi
Feedback loopWatch mode and hot module replacement
ExtensibilityA plugin system
DebuggingDebugger support for local development
Supply chainA dependency policy that decides which packages a build may ship
PermissionsPermission tracing, which prints the esrun line that grants exactly what a run used

Permission tracing connects the two binaries. Develop without flags, run esdev --trace-permissions app.js to see which capabilities the service actually used, then deploy with exactly that set on esrun. The esdev documentation covers the details.

Built in

Much of a typical service's dependency tree is available directly in the runtime:

  • Networking: HTTP and WebSocket servers.

  • Data stores: embedded SQLite, plus first-party clients for PostgreSQL, MySQL/MariaDB, MongoDB (beta), Redis and Valkey.

  • Messaging: AMQP 0-9-1 (RabbitMQ), MQTT and NATS.

  • Formats: XML, YAML, JSONC, JSON5, TOML, CSV, MessagePack, CBOR and Protobuf.

  • Utilities: SMTP email, archives, password hashing, two-factor codes, cookies, CSRF tokens and webhooks.

  • Media and documents: native image processing and a PDF toolkit.

  • Observability: OTLP span export to any collector, without an SDK.

  • WebAssembly support.

Each of these is code we maintain and hold to the same security expectations as the runtime itself, rather than another package to audit.

Durable workers

Durable workers give you addressable state that outlives the process, without extra infrastructure.

  • Serialized calls. Calls to a worker run one at a time, so concurrent requests cannot break an invariant such as a stock level.

  • Committed results. A call returns only after its writes have committed.

  • Persistent timers. Timers survive restarts.

  • No extra servers. Each worker stores its state in its own embedded SQLite file, so there is no separate queue, cache or database server to run.

See the durable workers guide for details.

Performance

Correctness comes first at OTF, and we do not build for benchmark numbers. We still measure performance, and we publish the results and the method, including the tests where ES Runtime is not the fastest.

The figures below are a selection, measured with esrun 0.45.0, Node.js 24.21.0, Bun 1.4.3 and Deno 2.9.7 on Linux with an Intel Core i7-8700K.

Startup and module loading

Lower is better.

RuntimeCold startMemory300-module graphMemory
Bun4.2 ms12 MB12.1 ms24 MB
esrun8.9 ms26 MB33.7 ms28 MB
Deno13.1 ms29 MB31.4 ms33 MB
Node.js20.3 ms43 MB94.6 ms62 MB

HTTP server

Hello-world requests per second; higher is better. Measured over loopback with oha at 100 connections, best of three runs.

FrameworkBunDenoesrunNode.js
Hono70.9k71.3k49.0k31.1k
Elysia81.2k75.0k50.2k30.7k

Bun and Deno lead here, and we say so plainly.

Database queries

Queries per second with 100 queries in flight, each returning 100 rows; higher is better. esrun uses its first-party drivers. Bun uses its built-in SQL client, while Node.js and Deno use postgres.js and mysql2.

DatabaseesrunBunDenoNode.js
PostgreSQL20.4k (100 MB)13.9k (66 MB)9.2k (145 MB)8.6k (212 MB)
MySQL22.1k (91 MB)19.2k (63 MB)15.9k (371 MB)14.8k (440 MB)

PDF processing

Eight documents processed at once from a 200-page report, with peak memory in brackets; lower is better. esrun uses its built-in runtime:pdf. The other runtimes have no built-in PDF support, so they run pdf.js for text extraction and pdf-lib for editing.

TaskesrunBunDenoNode.js
Extract text280 ms (60 MB)1,931 ms (169 MB)2,343 ms (259 MB)3,196 ms (376 MB)
Merge42 ms (93 MB)461 ms (174 MB)451 ms (150 MB)531 ms (189 MB)
Split20 ms (49 MB)264 ms (126 MB)213 ms (100 MB)319 ms (143 MB)

Results depend on workload and hardware, and different runtimes lead different tests. Treat these figures as a guide. The full benchmark suite has the method, the remaining results, and comparisons with LLRT.

Scope

ES Runtime is not a drop-in replacement for Node.js. Node.js compatibility, a package installer, a linter or formatter, and native extensions are all explicit non-goals.

It is aimed at teams that want a standards-based server platform with a hard security boundary and an integrated toolchain. For a side-by-side view of how it differs from Node.js, Bun and Deno, see the comparison page.

Getting started

ES Runtime is an alpha release, so interfaces may change as we incorporate feedback.

  1. Follow the documentation to install both binaries and run your first module.

  2. Read the security guide to build a least-privilege deploy line.

  3. Review the benchmarks and the comparison with Node.js, Bun and Deno.

  4. Report issues and contribute on GitHub.

Feedback of any kind is welcome. Tell us where ES Runtime falls short, what you would need to adopt it, and where its guarantees do not hold up.

Thanks for reading! 🙏