esrun vs Node.js · Bun · Deno
Node.js, Bun, and Deno are general-purpose runtimes. esrun is narrower: a secure, standards-first server runtime with deny-by-default permissions. Dev tooling lives in esdev, never in production.
Runtimes measured
esrun 0.38.0
Node.js 24.21.0 (LTS)
Bun 1.4.2
Deno 2.9.7
esdev 0.17.0 (development binary)
Legend
Supported Partial / flagged / experimental Not supported
Language, modules and Web standards
What the production binary loads and runs. Transforms live in esdev — see Development binary.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
| ES Modules | ||||
| JSON module imports | ||||
| Web APIs (fetch/URL/streams/WebCrypto) | ||||
| Temporal API | ||||
| WebAssembly | ||||
.wasm ES module imports | ||||
| WASI preview 1 |
On the Temporal row. Node 26+ only — the LTS (24) measured here has none.
Development binary (esdev)
TypeScript, JSX, CommonJS, bundling, tests and Tailwind live here — the local-only binary. esrun runs the bundle; nothing below ships to production.
| Capability | esdev | Node.js | Bun | Deno |
|---|---|---|---|---|
| TypeScript | ||||
| JSX | ||||
| CommonJS (require) | ||||
| Bundler | ||||
| Test runner | ||||
| Tailwind CSS | ||||
| Package installer |
CommonJS works because esdev converts packages to ES modules on first import; esrun runs the bundle where that is already settled.
TypeScript on Node strips erasable syntax only — enum and namespace need --experimental-transform-types.
Bundler: deno bundle works but warns it is experimental — hence
Tailwind: esdev compiles v4 with zero config (docs); Bun needs bun-plugin-tailwind; Node and Deno leave CSS to other tools.
Package installer: esdev has none by design — use your package manager.
HTTP and networking
The server, the protocols it speaks, and the socket surface around it.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
| Built-in HTTP server | ||||
| HTTP/2 server (cleartext h2c + ALPN) | ||||
| HTTP/3 server (QUIC) | ||||
| Cookie parse / serialize API | ||||
| Signed / sealed cookies | ||||
| CSRF tokens | ||||
| Webhook signing (Standard Webhooks) | ||||
| TOTP two-factor codes | ||||
| Stalled-connection timeouts by default | ||||
| UDP datagram sockets | ||||
| UDP multicast join/leave | ||||
| Connected UDP sockets |
On the HTTP/2 row. esrun and Deno serve both versions from one serve(); Node needs a separate node:http2 server, and Bun only via node:http2 — its own Bun.serve is HTTP/1.1-only. Throughput: Benchmarks.
On the stalled-connection row. Measured: esrun, Bun and Node close a connection that sends nothing; Deno holds it open. Node is headersTimeout on a 30s check lets one linger ~90s. Per-runtime times: HTTP server comparison.
On the HTTP/3 row. Only Bun serves it (experimental, hence node:quic on Node LTS; Deno 2.9 exposes no QUIC API at all. QUIC is a new UDP transport with its own TLS stack — a new listener, not a flag — so esrun has none yet.
On the cookie row. esrun and Bun ship Cookie/CookieMap; Deno's is a stdlib package, Node has none. Unlike Bun's, esrun's throws on cookies a browser would drop (SameSite=None without Secure, __Host- with Domain). See Cookies.
On the signed / sealed row. esrun signs (HMAC-SHA256) and seals (AES-256-GCM) with key rotation and a server-checked expiry. Deno only signs, through the unstable @std/http/unstable-signed-cookie; Bun 1.4.3 and Node 24 have neither.
On the CSRF row. esrun and Bun ship CSRF.generate/verify with the same shape. esrun requires the secret and the session ID, and checks Sec-Fetch-Site and Origin when given the request; Bun makes both optional. Deno's @std/http 1.1.4 and Node 24 have none. See CSRF.
On the webhook row. esrun signs and verifies Standard Webhooks, HMAC (v1) and Ed25519 (v1a), byte-compatible with the spec's reference library. Bun 1.4.3, Node 24 and Deno's @std/http and @std/crypto have no webhook API. See Webhooks.
On the TOTP row. esrun generates and checks RFC 6238 codes, writes otpauth:// URIs and makes backup codes, matching the otpauth library's codes and URIs. Bun 1.4.3, Node 24 and Deno's @std/crypto have no TOTP API; Deno's @std/encoding has base32 only. See Two-factor sign-in.
On the UDP rows. All four have it. Deno's stable path is node:dgram; Bun's surface is widest (sendMany, source-specific membership); esrun's is the only one where binding and sending are separate grants. Call-by-call mapping: UDP guide.
Data and built-in backends
Databases, images and mail: in the runtime, in a first-party package, or npm.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
| Built-in SQLite | ||||
| Built-in Postgres / MySQL client | ||||
| Built-in Redis client | ||||
| Runtime-provided database driver kit | ||||
| First-party SMTP client | ||||
| Built-in image processing |
On the database rows. postgres and mysql live in runtime:db; Redis (@opentf/esrun-redis) is a first-party package. All sit on the public driver tier below; Bun exports no such tier, Node and Deno leave drivers to npm.
On the SMTP row. First-party package @opentf/esrun-smtp over runtime:net. Elsewhere mail is npm, where nodemailer needs node:net, which esrun lacks.
On the image-processing row. Node has no image API; Deno encodes PNG/JPEG only. Per-operation breakdown: Images, feature by feature.
Workers, state and observability
Concurrency, isolation between workers, durable state, and self-exports.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
| Workers / multi-thread | ||||
| Per-worker memory limit | ||||
| Per-worker capabilities | ||||
worker.unref() | ||||
| Worker queue depth (backpressure signal) | ||||
| Durable workers (addressable state, alarms) | ||||
| Built-in OpenTelemetry export |
On the worker rows. Deno's web Worker has no unref() or memory limit (node:worker_threads has both); Bun accepts resourceLimits but never enforces it, per its own compatibility table.
On the durable workers row. Addressable instances with storage, alarms and hibernating WebSockets — the Durable Objects model, self-hosted. The other three leave state and scheduling to a database and job queue. See Durable workers.
On the OpenTelemetry row. esrun --otel exports OTLP/HTTP spans with no SDK; Deno (OTEL_DENO=true) also covers logs and metrics. Node and Bun need the npm SDK; Bun's native tracing is unreleased.
Security
The sandbox default and the flags that shape it. Full breakdown below in Permission models.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
| Capability sandbox (deny by default) | ||||
| Runtime permission flags (allow/deny) |
On the permission row. This is the CLI; the sandbox row above is the default. Both esrun and Deno deny by default — the table below shows how differently they grant, scope and query.
esrun sandbox in one line:
esrun app.jsholds nothing —--allow-*names what a run may reach.esdevgrants everything, so no flags indoors.
Node.js compatibility
esrun runs standards-only code — no node: builtins, FFI, or native addons. Porting means leaving those behind: From Node.js.
| Capability | esrun | Node.js | Bun | Deno |
|---|---|---|---|---|
Node.js compatibility (node: builtins) | ||||
| FFI (dlopen) | ||||
| Native addons (N-API) |
On these rows. Node LTS has no node:ffi at all; Bun (bun:ffi) and Deno (Deno.dlopen) do. Both report incomplete node: coverage (Bun lists partial modules, Deno passes 75%+ of Node's suite).
Permission models
| esrun | Node.js | Bun | Deno | |
|---|---|---|---|---|
| Restricted by default | ||||
| Turned on by | always on (-A off) | --permission | — | always on (-A off) |
| Direction | allow-list, or --allow-all + --deny-* | allow-list | — | allow-list + --deny-* |
| Scoped to paths / hosts | — | |||
| Interactive prompt | — | |||
| Entry file readable | — | |||
| Local imports readable | --allow-imports | — | ||
| Allow/deny which modules load | --import-policy file | — | ||
| Query from JS | permissions.has() | process.permission.has() | — | Deno.permissions.query() |
| Drop privileges at runtime | .drop() | — | .revoke() | |
| Filesystem root jail | always on | |||
| Status | alpha | stable (v22.13+) | requested | stable |
Rows 6–8: entry file runs, second file needs a grant (Deno exempts analysable imports). What the loader may resolve is separate: esrun's
--import-policy(packages/paths) vs Deno's--allow-import(remote hosts); Node removed theirs.Names:
read,write,imports,net,listen,env,run,signals,workers,diagnostics,diagnostics-detail— flags,permissions.has(), Worker options. See Security model.
Images, feature by feature
Each runtime's own image API — runtime:images, Bun.Image, Deno's createImageBitmap, and sharp for Node, which has none. Every cell was produced by running the operation, not by reading docs.
bench/images/features/features.py, Linux x86-64: esrun 0.38.0, sharp 0.35.5 on Node 24.21.0, Bun 1.4.2, Deno 2.9.7 (2026-10-08). Bun's AVIF/TIFF follow the OS codecs, so macOS/Windows differ.
Decoding
esrunruntime:images | Node.js sharp | BunBun.Image | DenocreateImageBitmap | |
|---|---|---|---|---|
| Decode JPEG | ||||
| Decode PNG | ||||
| Decode WebP | ||||
| Decode AVIF | ||||
| Decode GIF | ||||
| Decode TIFF | ||||
| Decode HEIC |
Encoding
esrunruntime:images | Node.js sharp | BunBun.Image | DenocreateImageBitmap | |
|---|---|---|---|---|
| Encode JPEG | ||||
| Encode PNG | ||||
| Encode WebP | ||||
| Encode AVIF | ||||
| Encode GIF | ||||
| Encode TIFF | ||||
| Encode HEIC |
Transforms
esrunruntime:images | Node.js sharp | BunBun.Image | DenocreateImageBitmap | |
|---|---|---|---|---|
Resize: cover | ||||
Resize: contain | ||||
Resize: outside | ||||
| Crop | ||||
| Rotate 90° | ||||
| Rotate 45° (any angle) | ||||
| Blur | ||||
| Composite | ||||
| Text overlay | ||||
| Animated output keeps frames | ||||
| Progressive JPEG |
Behaviour & safety
esrunruntime:images | Node.js sharp | BunBun.Image | DenocreateImageBitmap | |
|---|---|---|---|---|
| EXIF orientation applied by default | ||||
| Refuses a decompression bomb | ||||
| Error code on malformed input | ||||
| Reports animation frames | ||||
| Event loop runs while decoding |
Formats: the six everywhere on esrun/sharp; Bun follows the OS; Deno writes PNG/JPEG only (WebP/AVIF requests return PNG per spec).
HEIC: esrun
ERR_IMAGE_FORMAT_UNSUPPORTED— convert to AVIF/JPEG; Bun ,img.heic({ quality: 80 })on macOS/Windows; sharp here lacks the HEVC plugin; Deno none.Operations: thumbnails only — no free rotation, compositing, text, multi-frame output, progressive JPEG, or encoder knobs. Beyond that, use sharp.
Safety: 400 MP bomb refused at header by all but Deno; sharp skips EXIF unless asked and its errors carry no code; Deno decode blocks the loop.
Speed: Benchmarks.
Redis clients, feature by feature
Same table as the Redis guide, where usage lives. Every cell was produced by running the client. Node/Deno show both npm clients — redis (official) and ioredis (~2× downloads) — because they differ enough to misreport Node alone.
bench/db/redis/features.mjs vs Redis 8.10.2: node-redis 6.2.1, ioredis 6.0.0, Bun 1.4.2, esrun 0.32.0 (2026-09-26). Cluster/Sentinel/private-CA rows check API presence — one server cannot exercise them.
esrunesrun-redis | Node/Deno node-redis 6 | Node/Deno ioredis 6 | Bun built-in | |
|---|---|---|---|---|
| Ships with the runtime | ||||
| RESP3 negotiated by default | ||||
| Exact 64-bit integers | ||||
| Binary-safe values | ||||
| Pub/sub | ||||
| Pipeline builder | ||||
MULTI/EXEC API | ||||
WATCH | ||||
| Cluster | ||||
| Sentinel | ||||
| Streams & consumer groups | ||||
| TLS with a private CA | ||||
| Auto-reconnect | ||||
| Unbounded-block guard | ||||
| Portable error codes | ||||
| Scoped by a capability flag | Deno only | Deno only |
Exact 64-bit integers is the only row that changes an answer:
…993nhere,…992(off by one) elsewhere — Redis integers are 64-bit.The
cells: no pipeline builder on node-redis/Bun (Promise.allis the idiom); Bun has nomulti/watch/cluster/Sentinel; esrun auto-reconnect is off by default (a pool replaces dead connections).Last three rows are esrun-only: no-timeout blocks refused, portable error codes,
--allow-netscoping.
Speed
Full numbers: Benchmarks. Bun's native client leads (1.3× round trips, 3–5× decoding). Among JS clients esrun is fastest where the round trip dominates at a third of the memory — but 2× slower on repeated HGETALL, a fixable map-decoding cost in this driver.
Where the API surface differs
No Node.js API. Only capability-gated
runtime:modules — nonode:fs,Buffer, or globalprocess.Globals are not host APIs. Global scope stays Web-platform; no fs/process on globals either (like Deno, stricter).
Always-on filesystem jail. Project-root confinement, symlink-proof, no off flag.
node_moduleswalk stops at the run directory, not the fs root.Two binaries.
esrunserves;esdevbundles, tests, watches, debugs. Same behavior core.WASI is enforced, not advisory. Preopen + capability + jail — Node's own docs call
node:wasinot a secure sandbox. See WebAssembly & WASI.
Status reflects general availability at the time of writing and is a summary, not an exhaustive audit. See Benchmarks for measured performance.