Benchmarks

Standard Web APIs, the same script on each runtime (n/a where a runtime lacks one). Lower is better.

Runtimes measured

  • Node.js v26.13.0
  • Bun 1.4.4+38afee6c8
  • Deno deno 2.9.7 (stable, release, x86_64-unknown-linux-gnu)
  • LLRT LLRT v0.9.0-beta (linux, x64)
  • esrun esrun 0.45.0
How it's measured
  • Runtimes run interleaved, in randomized order each repetition — one shared contention window.

  • Each cell is the minimum over repetitions (the contention-free floor), after a discarded warmup.

  • High-variance cells are flagged in the raw output.

Standings at a glance

Who placed where across every metric. Pick by fit, not by medal count.

RuntimeTests🥇🥈🥉4th5th
Bun7436171110·
esrun7492422163
Node.js73149211415
Deno7241715288
LLRT531174427

Across 74 comparable metrics, each ranked by its own better direction. Each runtime is ranked only on metrics it can run, so totals differ — e.g. LLRT has no HTTP server or filesystem here. Place counts are not a ranking: more 1st-place finishes does not mean “best overall.”

Startup & footprint

Process wall-time (min of N runs). bigscript is one large file, modules a 300-module graph; memory twice — idle, and with a live set retained.

Cold start (near-empty script)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
4.1ms
13MB
Deno
12.8ms
30MB
esrun
8.8ms
26MB
LLRT
3.5ms
11MB
Node.js
21.3ms
43MB
Parse + run ~100 KB script
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
14.2ms
17MB
Deno
21.3ms
30MB
esrun
20.3ms
27MB
LLRT
11.2ms
15MB
Node.js
31ms
46MB
Load a 300-module graph
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
11.9ms
24MB
Deno
30.9ms
33MB
esrun
33.2ms
28MB
LLRT
15.1ms
13MB
Node.js
91.1ms
64MB
Peak resident memory (idle)
Runtime
Performance (lower is better)
Bun
13MB
Deno
30MB
esrun
26MB
LLRT
11MB
Node.js
43MB
Peak resident memory (under load)
Runtime
Performance (lower is better)
Bun
136MB
Deno
129MB
esrun
110MB
LLRT
165MB
Node.js
139MB

Engine

Self-timed with performance.now() after an untimed JIT warmup — engine cost, not process launch; min of N interleaved runs.

Tight compute loop
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
110.4ms
22MB
Deno
231.3ms
40MB
esrun
189.5ms
38MB
LLRT
1686.7ms
11MB
Node.js
193.8ms
55MB
JSON parse/stringify
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
144.7ms
38MB
Deno
189.9ms
57MB
esrun
189.2ms
55MB
LLRT
617.5ms
11MB
Node.js
193.8ms
71MB
JSON (large documents)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
372.9ms
107MB
Deno
497.5ms
139MB
esrun
465.2ms
94MB
LLRT
1526ms
139MB
Node.js
495.6ms
152MB
Regex (match, validate, replace)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
12.3ms
31MB
Deno
61.8ms
41MB
esrun
57.6ms
40MB
LLRT
926.9ms
11MB
Node.js
63.8ms
57MB
String building and slicing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
57.4ms
42MB
Deno
59ms
49MB
esrun
55.3ms
48MB
LLRT
156.9ms
11MB
Node.js
52.4ms
64MB
structuredClone
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
222.4ms
30MB
Deno
249ms
44MB
esrun
265.5ms
53MB
LLRT
380.5ms
11MB
Node.js
194.6ms
56MB
throw/catch + stack capture
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
274.9ms
34MB
Deno
4194.1ms
65MB
esrun
366.8ms
48MB
LLRT
362.5ms
11MB
Node.js
1263.8ms
65MB
async/await throughput
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
38ms
31MB
Deno
29.6ms
40MB
esrun
30ms
40MB
LLRT
540.1ms
11MB
Node.js
46.3ms
57MB
setTimeout churn
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
29.1ms~
46MB
Deno
234.1ms
168MB
esrun
101ms
91MB
LLRT
35.3ms
51MB
Node.js
35.6ms~
87MB

~ varied more than 10% run to run; read as approximate.

Web APIs

URL parsing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
30.5ms
47MB
Deno
97.1ms
48MB
esrun
86.9ms
48MB
LLRT
117.1ms
13MB
Node.js
31.8ms
65MB
URL setter
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
84ms
47MB
Deno
181.2ms
47MB
esrun
182.9ms
48MB
LLRT
111.4ms
11MB
Node.js
116.1ms
64MB
URLPattern test
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
420.9ms
115MB
Deno
5026.5ms
66MB
esrun
802.2ms
44MB
LLRT
—
—
Node.js
324.5ms
73MB
TextEncoder/TextDecoder (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.3ms
35MB
Deno
66.9ms
51MB
esrun
62.7ms
50MB
LLRT
67ms~
11MB
Node.js
70.5ms
66MB
TextEncoder/TextDecoder (64 KiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
68.4ms
37MB
Deno
142ms
40MB
esrun
100ms
40MB
LLRT
240.5ms
15MB
Node.js
73.5ms
56MB
base64 (atob/btoa)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
9.7ms
36MB
Deno
7.3ms~
43MB
esrun
22.1ms
39MB
LLRT
33.1ms
11MB
Node.js
7.1ms
56MB
TypedArray / DataView
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.5ms~
33MB
Deno
13ms
42MB
esrun
12.5ms
40MB
LLRT
69.2ms
13MB
Node.js
13.4ms
57MB
Headers / Request parsing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
167.7ms
35MB
Deno
1496.8ms
127MB
esrun
421.9ms
50MB
LLRT
—
—
Node.js
400.9ms
78MB
Multipart FormData round trip
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
20.3ms
46MB
Deno
372.3ms
112MB
esrun
88ms
45MB
LLRT
4170.4ms
16MB
Node.js
227.4ms
128MB
Date + Intl formatting
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
75.4ms
38MB
Deno
137ms
48MB
esrun
136.5ms
45MB
LLRT
—
—
Node.js
127.4ms
65MB
ReadableStream piping
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
5.6ms
25MB
Deno
10.4ms
45MB
esrun
9.8ms
45MB
LLRT
90.4ms
11MB
Node.js
7.7ms
65MB
CompressionStream round trip (gzip)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
26.7ms
39MB
Deno
77.6ms
56MB
esrun
71.5ms
54MB
LLRT
—
—
Node.js
279.2ms
75MB

~ varied more than 10% run to run; read as approximate.

Crypto

SubtleCrypto SHA-256
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
400.1ms
35MB
Deno
400.2ms
42MB
esrun
331.2ms
37MB
LLRT
323.7ms
11MB
Node.js
492.1ms
60MB
HMAC + AES-GCM
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
82.1ms
32MB
Deno
100.4ms~
43MB
esrun
34ms
38MB
LLRT
34.4ms
14MB
Node.js
142.1ms
61MB
ECDSA P-256 sign + verify
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
188.2ms
25MB
Deno
2164.7ms
43MB
esrun
1816.8ms
37MB
LLRT
1000.4ms
12MB
Node.js
305.9ms
60MB
PBKDF2 (10k iterations)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
62.7ms
19MB
Deno
69.9ms
35MB
esrun
97.3ms
30MB
LLRT
95.4ms
14MB
Node.js
71.3ms
51MB

~ varied more than 10% run to run; read as approximate.

Hashing

crypto above is async crypto.subtle; these rows are what the caller wanted — encoded hashes, synchronously. runtime:hashing on esrun, Bun.CryptoHasher, node:crypto elsewhere; hash_fast only exists on esrun and Bun.

SHA-256 to hex (one-shot)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
173ms
33MB
Deno
179.1ms
56MB
esrun
311ms
40MB
LLRT
314.5ms
11MB
Node.js
159.4ms
62MB
SHA-256 over 64 KiB chunks (4 MiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
1640.2ms
21MB
Deno
1641.1ms
45MB
esrun
3036.2ms
33MB
LLRT
3033.9ms
13MB
Node.js
1457.8ms
53MB
Non-cryptographic hash (64 KiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
46.1ms
23MB
Deno
—
—
esrun
69.4ms
41MB
LLRT
—
—
Node.js
—
—
Same work, two pipelines

hash_hex and sha256 are the same work — 20 000 × SHA-256 of one 4 KiB buffer — async vs sync (esrun 7% apart, Node 3.2×: crypto.subtle here is the sync op with a resolved promise around it). The remaining ~2× is RustCrypto's portable Rust vs OpenSSL's AVX2 assembly — on SHA-NI hardware, which this box is not, it's a different measurement.

Network

HTTP server runs client and server in one process, so it mixes both halves. bench/rps.sh measures the server alone, externally.

fetch (local server)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
15.8ms
21MB
Deno
32.7ms
43MB
esrun
40.1ms
42MB
LLRT
15.6ms~
13MB
Node.js
88.5ms
86MB
fetch (streamed upload)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
37.9ms
25MB
Deno
33.3ms~
46MB
esrun
39.2ms
41MB
LLRT
8631.9ms
14MB
Node.js
94ms
84MB
HTTP server (concurrent)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
49.5ms
31MB
Deno
104.9ms
72MB
esrun
113.8ms
66MB
LLRT
—
—
Node.js
332.5ms
146MB
WebSocket round trips
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
418.4ms
32MB
Deno
557.4ms
47MB
esrun
815.3ms
43MB
LLRT
—
—
Node.js
586.5ms
78MB
UDP round trips (10 000 datagrams)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
168.1ms~
32MB
Deno
405.1ms
67MB
esrun
337.9ms
42MB
LLRT
329.5ms
13MB
Node.js
263.7ms
63MB
UDP send throughput (50 000 datagrams)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
182.9ms
25MB
Deno
251.9ms
43MB
esrun
208.5ms
42MB
LLRT
788.7ms
13MB
Node.js
228.2ms
63MB

~ varied more than 10% run to run; read as approximate.

Hello world, measured externally

JSON hello-world through Hono per runtime (LLRT has no HTTP server), driven externally on split cores — oha/bombardier, since autocannon caps everything at ~35–40k. Memory is whole-process peak RSS: footprint, not attribution.

Hono hello-world · Speed & Memory
Runtime
Throughput (higher ↑)
Memory (lower ↓)
Bun
82.1k
45 MB
Deno
78.3k
53 MB
esrun
49.3k
55 MB
Node.js
40.4k
100 MB

Burst vs sustained

The chart is a burst against a fresh process; held under load, the heap is full and the collector never stops.

RuntimeBurst req/s60s req/sChange
Bun82.1k76.2k-7.2%
Deno78.3k69.6k-11.1%
esrun49.3k50.1k+1.5%
Node.js40.4k40.5k+0.4%

Static files, measured externally

A 64 KiB file per response, external load generator on separate cores. Bun and Deno hand a file handle to the kernel; Node streams; esrun reads host-side.

HTTP requests/sec · 64 KiB static file
Runtime
Throughput (higher ↑)
Memory (lower ↓)
Bun
30.6k
36 MB
Deno
14.1k
149 MB
esrun
18.4k
71 MB
Node.js
13.2k
168 MB
Two samples, not one table

Bare plain-text is the earlier sample, Hono JSON the 2026-10-01 refresh — compare down a column, never across it. Higher is better.

RuntimeBare server req/secThrough Hono req/sec
Deno85,07069,741
Bun82,61573,951
esrun49,53750,601
Node29,55830,872

The Hono column serves {"message":"Hello, World!"} through unmodified npm ESM off node_modules — esrun at 50,601 req/s against Bun's 73,951.

esrun, Bun and Deno each saturate ~one core here — a per-request gap, not a core-count one. The stable measure is server CPU time per request:

ServerCPU µs/req~req/s on 1 core
Bare hyper, no JS (transport floor)~10.4—
Deno~11.9~84k
Bun~12.2~82k
esrun~18.2~55k
Node~33.8~30k
The ~8µs seam

Bun/Deno add ~2µs of JS-handler overhead over bare hyper; esrun adds ~8µs — the injectable-provider + driven-loop seam, tuned from ~29µs via batched accept, structured metadata, lazy bodies/headers, and the host-validated URL.

Filesystem

Page-cache resident, no fsync — the runtime's cost above the syscall, not disk speed. Numbers are specific to the filesystem in environment.

File read (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
37.5ms
34MB
Deno
42ms
43MB
esrun
44.9ms
40MB
LLRT
26.7ms
13MB
Node.js
46.4ms
56MB
File read (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
17.3ms
37MB
Deno
56.9ms~
168MB
esrun
27.4ms
92MB
LLRT
7ms
21MB
Node.js
63ms
115MB
File write (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
12.4ms
23MB
Deno
80ms
42MB
esrun
80ms
35MB
LLRT
89.4ms
13MB
Node.js
90.4ms
60MB
File write (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
18.3ms
23MB
Deno
38.8ms~
82MB
esrun
36.8ms
36MB
LLRT
38.7ms~
21MB
Node.js
46.4ms~
95MB
File append (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
29.9ms~
25MB
Deno
38.5ms
43MB
esrun
37.4ms
35MB
LLRT
—
—
Node.js
52.7ms
63MB
File append (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
6.3ms
21MB
Deno
15.6ms
52MB
esrun
8.7ms
29MB
LLRT
—
—
Node.js
16.1ms
65MB
File stat (one path)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
48.7ms
25MB
Deno
89.3ms
42MB
esrun
55ms
36MB
LLRT
38.4ms
13MB
Node.js
68.1ms
52MB
File stat (1000 paths)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
199.4ms
33MB
Deno
357.2ms
46MB
esrun
242.5ms
42MB
LLRT
154ms~
13MB
Node.js
276.1ms
62MB
File exists (one path)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
7.3ms
26MB
Deno
88.5ms
42MB
esrun
42.1ms
36MB
LLRT
46.6ms
13MB
Node.js
56.4ms
52MB
File exists (1000 paths)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
30.1ms
36MB
Deno
361.9ms
47MB
esrun
190ms
37MB
LLRT
183.2ms
13MB
Node.js
234.2ms
63MB
Glob scan
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
24.7ms
32MB
Deno
—
—
esrun
47.8ms
37MB
LLRT
—
—
Node.js
185.7ms
69MB

~ varied more than 10% run to run; read as approximate.

System

One child process per call — start /bin/echo, drain stdout, wait. The bytes back are the runtime's half; fork/exec is the kernel's.

Spawn a child process
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
98.5ms
24MB
Deno
107.9ms
52MB
esrun
81.5ms
37MB
LLRT
94.9ms
13MB
Node.js
183ms
54MB

Tracing and async context

Measured separately by bench/probe-tracing.sh; shown where each design is explained:

  • Async context — propagation cost vs Node, Bun, Deno, incl. the known promise-hook overhead.

  • Diagnostics — one op idle, subscribed, and under --otel.

Behaviour under memory exhaustion

Not speed but survival: a catchable error is a failed request, a signal is a dropped process.

ScenarioBunDenoesrunLLRTNode.js
200k-deep nested array → JSON.stringifygracefulgracefulgracefulSIGSEGVgraceful
String doubled past the engine maximumgracefulgracefulgracefulgracefulgraceful
10M chained .then()gracefulgracefulgracefulgracefulgraceful

Native vs third-party parsers

Each runtime parses with the best facility it ships; where one ships nothing, the library you'd reach for anyway. Native-vs-library gaps, not runtime-vs-runtime.

JSONL streaming (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
846.7ms
82MB
Deno
658.1ms
147MB
esrun
598.7ms
71MB
LLRT
—
—
Node.js
583.1ms
160MB
JSONC parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
61.6ms
39MB
Deno
273.6ms
101MB
esrun
68.1ms~
52MB
LLRT
2713.9ms
39MB
Node.js
189.5ms
126MB
JSON5 parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
118ms
61MB
Deno
1071ms
129MB
esrun
94.4ms
58MB
LLRT
9785.6ms
25MB
Node.js
1070.4ms
138MB
XML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
410.3ms
78MB
Deno
412.9ms
70MB
esrun
153.5ms
34MB
LLRT
56.1ms
13MB
Node.js
467.3ms
79MB
XML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
887.1ms
107MB
Deno
850ms
207MB
esrun
310.5ms
84MB
LLRT
112.7ms
23MB
Node.js
962.7ms
218MB
YAML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
96.6ms
47MB
Deno
181.7ms
49MB
esrun
209.3ms
37MB
LLRT
4129.4ms
15MB
Node.js
196.8ms
65MB
YAML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
188.1ms
60MB
Deno
364.4ms
125MB
esrun
403.7ms
60MB
LLRT
8294.1ms
25MB
Node.js
382.2ms
138MB
TOML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
51.6ms
51MB
Deno
224.2ms
61MB
esrun
152.1ms
36MB
LLRT
3728.6ms
16MB
Node.js
200.2ms
69MB
TOML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
100.4ms
62MB
Deno
469.5ms
201MB
esrun
312.8ms
66MB
LLRT
7572.9ms
22MB
Node.js
442ms
209MB
MessagePack parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
44.7ms
53MB
Deno
37.5ms
60MB
esrun
52.6ms
37MB
LLRT
792.2ms
15MB
Node.js
38.4ms
64MB
MessagePack parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
44.9ms
62MB
Deno
39.9ms~
76MB
esrun
55.5ms
66MB
LLRT
788.8ms
21MB
Node.js
38.7ms
83MB
CBOR parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
49.3ms
53MB
Deno
291.2ms
55MB
esrun
109ms
36MB
LLRT
—
—
Node.js
39.2ms
63MB
CBOR parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
46.9ms
67MB
Deno
315.7ms
127MB
esrun
107.7ms
59MB
LLRT
—
—
Node.js
40.6ms
79MB
CSV parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
123.8ms
60MB
Deno
73.8ms
50MB
esrun
34.2ms
36MB
LLRT
—
—
Node.js
92.5ms
68MB
CSV parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
178.6ms
74MB
Deno
160.5ms
118MB
esrun
68ms
45MB
LLRT
—
—
Node.js
156.8ms
83MB
CSV streaming (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
1078.1ms
87MB
Deno
561.3ms
97MB
esrun
439.6ms
93MB
LLRT
—
—
Node.js
924.2ms
103MB

~ varied more than 10% run to run; read as approximate.

Who runs what

esrun's runtime:serialization, Bun's native Bun.YAML/Bun.TOML, LLRT's llrt:xml; js-yaml, @iarna/toml, fast-xml-parser, msgpackr, cbor-x, Deno's @std/cbor/@std/csv, and csv-parse on Node and Bun.

Protobuf (each runtime's own path)

esrun's native runtime:serialization Protobuf (pure-JS, reflective) against protobuf-es elsewhere. Identical bytes, each runtime's real path.

Protobuf decode (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
72.3ms~
56MB
Deno
70.9ms
53MB
esrun
74.8ms
46MB
LLRT
964.2ms
18MB
Node.js
57.6ms
69MB
Protobuf decode (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
391.7ms
154MB
Deno
401.6ms
239MB
esrun
394.6ms
172MB
LLRT
4902.5ms
88MB
Node.js
324.4ms
215MB

~ varied more than 10% run to run; read as approximate.

WebAssembly

V8's own WebAssembly on esrun, Node and Deno; JavaScriptCore on Bun; LLRT has none.

WebAssembly.compile (~250 KB module)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
62.4ms
142MB
Deno
35.8ms
221MB
esrun
38.2ms
152MB
LLRT
—
—
Node.js
38.7ms
243MB
JS↔wasm call boundary
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
149ms
29MB
Deno
78.9ms
43MB
esrun
78.9ms
40MB
LLRT
—
—
Node.js
77.4ms
57MB
wasm linear memory (shared buffer)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
363.2ms
29MB
Deno
237.9ms
45MB
esrun
249.1ms
41MB
LLRT
—
—
Node.js
199.7ms
58MB
RowWhat it measures
CompileValidation + baseline codegen on a ~250 KB module, salted per iteration — no code cache.
Call boundary20M JS↔wasm calls into a trivial exported add.
Linear memoryJS fills a 64 KiB window, wasm sums it back.

WASI

wasm32-wasip1, as a compiled CLI targets it. Bootstrap is 2,000 runs with compilation hoisted out; syscalls loop 20,000 random_get + clock_time_get from inside wasm.

WASI bootstrap (instantiate + _start)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
389.8ms
34MB
Deno
46ms
63MB
esrun
44.5ms
42MB
LLRT
—
—
Node.js
361.3ms
78MB
WASI syscalls from inside wasm
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
13.9ms
43MB
Deno
18.1ms
54MB
esrun
55.4ms
44MB
LLRT
—
—
Node.js
42.8ms
56MB
Split verdict

Fastest bootstrap — ahead of Deno, several times Node/Bun — but on syscalls Deno is ~3× faster and Bun too: esrun's preview-1 is JS, theirs native.

WebSocket chat broadcast

C clients in one room; the server fans every message out. Cells are RECV messages/sec, best of 3 — higher is better.

Server fan-out

Each runtime's built-in server, fixed Bun client driver. Node has no built-in WS server.

ServerC=32C=64C=128
Bun253,663253,033254,755
Deno214,788214,998211,573
esrun275,124144,801107,324

esrun serves through the driven seam (one actor per connection), not native pub/sub — batched broadcast() leads low, the connection-map lock costs at scale.

Client throughput

Fixed Bun server; each runtime's client under the same load — esrun lands on par with Node, Bun and Deno.

ClientC=32C=64C=128
Bun253,239253,109252,681
Deno254,948255,126253,845
esrun260,258259,599258,454
Node.js259,818259,500262,983

See the WebSocket guide.

HTTP/1.1 vs HTTP/2

Each runtime's hello-world server over HTTP/1.1 and cleartext h2, regenerated by bench/http2.sh — a separate run, not another row. Cells are req/sec, best of 3.

ShapeWhat it is
Wide (50 conns × 1 stream)HTTP/1.1's best case — h2 is pure framing overhead, and every runtime loses.
Narrow (1 conn × 50 streams)What h2 is for (H1 is strictly serial here) — esrun leads outright, from the slowest H1 baseline.
RuntimeWide — 50 conns × 1 streamNarrow — 1 conn × 50 streams
HTTP/1.1HTTP/2GainHTTP/1.1HTTP/2Gain
Bun93,08638,5790.41׆31,29747,2621.51׆
Deno95,63022,6500.24×32,00142,4041.33×
esrun48,88839,7800.81×18,73269,2373.70×
Node.js50,57617,0900.34׆26,57245,5311.71׆
Which comparisons this table licenses

Compare down a column — one client shape, each runtime's best server. The gain column isolates the protocol only where the server didn't change (Node and Bun serve h2 from node:http2 †). Never compare gains across runtimes: a weaker H1 baseline inflates a multiple.

See the HTTP/2 section of the HTTP guide.

Redis clients

esrun with @opentf/esrun-redis against Bun's built-in client and both npm clients (redis and ioredis) on Node and Deno — they don't perform alike, so one can't stand in for the other.

Wall ms, min of 5, lower is better. Checksums agreed on all five workloads.

WorkloadesrunNode
node-redis
Node
ioredis
Bun
built-in
Deno
node-redis
Deno
ioredis
serial_set — 5 000 SET, one at a time85810299106661030886
serial_get — 5 000 GET, one at a time85410408846451032873
pipeline — 20 000 SET in one batch22537420372366186
list — LRANGE over 50 000 elements15820311928209111
hash — 200 × HGETALL of 1 000 fields957479496205459494

Peak RSS, MB:

WorkloadesrunNode
node-redis
Node
ioredis
Bun
built-in
Deno
node-redis
Deno
ioredis
serial_set38.4112.379.133.6124.499.6
serial_get38.8112.979.634.0123.599.2
pipeline123.8181.1129.144.3191.6139.5
list92.996.186.540.7103.886.7
hash78.7101.497.245.0114.1113.7
Where esrun wins

Fastest where the round trip dominates (858 ms vs 1029/910) at a third of the memory (38 vs 79–124 MB) — and ahead of node-redis on batch and list. Bun's native C++ leads everything: 1.3× on round trips, 3–5× on decode.

The hash gap, isolated

957 ms vs ~480 on repeated HGETALL — the map path only, not decoding (list scan is mid-pack) and not UTF-8 (binary: true was 2% faster). A RESP3 map crosses as a pair array of wrapper objects holding copied Uint8Arrays — ~4 allocations per field — where ioredis builds the same object twice as fast. Fixable implementation cost.

Unlike the rest of this page, these runs are not interleaved — treat small gaps as noise. Reproduce with bench/db/redis/run.sh.

SQLite

esrun's runtime:db (async, Turso 0.8.3) against synchronous in-process SQLite: node:sqlite on Node and Deno, bun:sqlite on Bun. Same schema, same rows, agreeing checksums.

Wall ms, min of 5, lower is better.

WorkloadesrunNodeBunDeno
open — fresh file + schema26.333.011.630.9
insert — 50 000 rows, one statement each1198.4187.3146.8204.1
insert_many — the same 50 000 in one batch391.1n/an/an/a
scan_num — full scan, 3 numeric columns41.498.522.1123.8
scan_text — full scan, 2 text columns52.191.525.5118.0
point — 5 000 lookups by id162.581.452.176.0
stream — 200 000-row scan through the cursor77.9201.947.6312.9

Peak RSS, MB:

WorkloadesrunNodeBunDeno
open36.246.818.536.0
insert64.359.544.552.3
insert_many149.6n/an/an/a
scan_num53.959.735.757.8
scan_text55.459.641.357.2
point47.350.227.541.3
stream61.965.441.064.9
Batch the writes

Per-statement crossings are esrun's cost — 1198 ms for 50 000 single-row executes vs ~150–200 sync — and insert_many is the answer: one crossing, 391 ms. The streaming scan lands second (78 vs 202/313); only point lookups lose outside the boundary (163 vs 52–81).

What runs where

One shared definition (bench/db/workload.mjs); insert_many is esrun-only, the sync APIs have no equivalent. Memory is mid-pack except the batch, which holds all rows in JS up front.

Unlike the rest of this page, these runs are not interleaved — treat small gaps as noise. Reproduce with bench/db/run.sh.

Images

40 thumbnails at once, as a server meets uploads — each runtime's own pipeline. Wall ms, min of 5.

What runs where

runtime:images, Bun.Image, Deno's createImageBitmap + OffscreenCanvas, sharp (libvips) on Node. Defaults everywhere, quality 80; checksums on output dimensions, refused on mismatch.

WorkloadBun
Bun.Image
Deno
createImageBitmap
esrun
runtime:images
Node.js
sharp
12 MP JPEG → 400 px WebP167 msn/a504 ms343 ms
12 MP JPEG → 400 px JPEG116 ms49,171 ms242 ms237 ms
1280×800 PNG → 320 px JPEG60 ms4,338 ms227 ms207 ms
12 MP JPEG → 400 px AVIFn/an/a674 ms6,515 ms
12 MP progressive JPEG → 400 px WebP438 msn/a931 ms850 ms
Progressive input costs more

pjpeg_webp decodes slower than jpeg_webp on every runtime — compare it only against itself (Bun leads).

Size of one output — a time is only comparable between similar sizes:

WorkloadBun
Bun.Image
Deno
createImageBitmap
esrun
runtime:images
Node.js
sharp
12 MP JPEG → 400 px WebP16.2 KBn/a16.5 KB16.1 KB
12 MP JPEG → 400 px JPEG24.4 KB35.9 KB23.3 KB23.1 KB
1280×800 PNG → 320 px JPEG16.4 KB19.9 KB16.4 KB15.4 KB
12 MP JPEG → 400 px AVIFn/an/a24.3 KB23.4 KB
12 MP progressive JPEG → 400 px WebP16.2 KBn/a16.5 KB16.1 KB

Whole-process peak RSS:

WorkloadBun
Bun.Image
Deno
createImageBitmap
esrun
runtime:images
Node.js
sharp
12 MP JPEG → 400 px WebP127.7 MBn/a150.4 MB99.7 MB
12 MP JPEG → 400 px JPEG109.6 MB199.1 MB103.6 MB100.6 MB
1280×800 PNG → 320 px JPEG123 MB64.6 MB342 MB143 MB
12 MP JPEG → 400 px AVIFn/an/a217.4 MB194.6 MB
12 MP progressive JPEG → 400 px WebP605.9 MBn/a1507.3 MB751.4 MB

Where esrun is slower, the time is in Pixels' codecs.

Whose codecs

esrun's are otf-pixels' own Rust; Bun and sharp use C libraries, Deno's encoder writes PNG and JPEG only. See Internals: images; reproduce with bench/images/run.sh.

PDF

Eight documents at once: the text of a 200-page report, the report merged with itself, and its first ten pages split off. Wall ms, min of 5.

What runs where

runtime:pdf on esrun. Node.js, Bun and Deno have no PDF support built in, so each runs pdf.js (pdfjs-dist) for text and pdf-lib to edit. Checksums on the extracted words and the pages written, refused on mismatch.

WorkloadBun
pdf.js + pdf-lib
Deno
pdf.js + pdf-lib
esrun
runtime:pdf
Node.js
pdf.js + pdf-lib
Text of a 200-page report1,910 ms2,360 ms283 ms2,663 ms
Merge it with itself (400 pages)464 ms465 ms42 ms528 ms
Split off its first 10 pages266 ms213 ms20 ms314 ms

Whole-process peak RSS:

WorkloadBun
pdf.js + pdf-lib
Deno
pdf.js + pdf-lib
esrun
runtime:pdf
Node.js
pdf.js + pdf-lib
Text of a 200-page report197.4 MB258.4 MB58.2 MB311.2 MB
Merge it with itself (400 pages)180.5 MB148.6 MB91.4 MB189.9 MB
Split off its first 10 pages124.2 MB99.8 MB47.3 MB143.8 MB

Reproduce with bench/pdf/run.sh; see Internals: PDF.

Reproduce

The harness lives in bench/, auto-detects runtimes, and emits the JSON behind these charts:

Shell
# human-readable table
bench/run.sh

# one section only — see `bench/run.sh --list`
GROUP=fs bench/run.sh

# regenerate the data module this page renders
bench/gen-bench-data.sh

# one section, or a few rows, merged into the rest
SECTIONS=rps_sustained bench/gen-bench-data.sh
bench/gen-bench-data.sh regex strings

# just the HTTP/1.1 vs HTTP/2 table
bench/http2.sh
Your hardware, your numbers

Numbers vary by machine — treat them as relative, not absolute. For CLI/script benchmarking, hyperfine is also a good fit.

Last updated on
Edit this page