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.

CapabilityesrunNode.jsBunDeno
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.

CapabilityesdevNode.jsBunDeno
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.

CapabilityesrunNode.jsBunDeno
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 — its 60s 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 ). No 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.

CapabilityesrunNode.jsBunDeno
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.

CapabilityesrunNode.jsBunDeno
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.

CapabilityesrunNode.jsBunDeno
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.js holds nothing — --allow-* names what a run may reach. esdev grants 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.

CapabilityesrunNode.jsBunDeno
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

esrunNode.jsBunDeno
Restricted by default
Turned on byalways on (-A off)--permission—always on (-A off)
Directionallow-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 JSpermissions.has()process.permission.has()—Deno.permissions.query()
Drop privileges at runtime.drop()—.revoke()
Filesystem root jailalways on
Statusalphastable (v22.13+)requestedstable
  • 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

esrun
runtime:images
Node.js
sharp
Bun
Bun.Image
Deno
createImageBitmap
Decode JPEG
Decode PNG
Decode WebP
Decode AVIF
Decode GIF
Decode TIFF
Decode HEIC

Encoding

esrun
runtime:images
Node.js
sharp
Bun
Bun.Image
Deno
createImageBitmap
Encode JPEG
Encode PNG
Encode WebP
Encode AVIF
Encode GIF
Encode TIFF
Encode HEIC

Transforms

esrun
runtime:images
Node.js
sharp
Bun
Bun.Image
Deno
createImageBitmap
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

esrun
runtime:images
Node.js
sharp
Bun
Bun.Image
Deno
createImageBitmap
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.

esrun
esrun-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 flagDeno onlyDeno only
  • Exact 64-bit integers is the only row that changes an answer: …993n here, …992 (off by one) elsewhere — Redis integers are 64-bit.

  • The cells: no pipeline builder on node-redis/Bun (Promise.all is the idiom); Bun has no multi/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-net scoping.

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 — no node:fs, Buffer, or global process.

  • 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_modules walk stops at the run directory, not the fs root.

  • Two binaries. esrun serves; esdev bundles, tests, watches, debugs. Same behavior core.

  • WASI is enforced, not advisory. Preopen + capability + jail — Node's own docs call node:wasi not 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.

Last updated on
Edit this page