Benchmarks

Standard Web APIs, the same script on each runtime (n/a where a runtime lacks one). Lower is better. esrun pairs near-instant startup with full WinterTC coverage and a JIT engine.

Runtimes measured

  • Node.js v24.14.0
  • Bun 1.4.2+744846f84
  • Deno deno 2.9.5 (stable, release, x86_64-unknown-linux-gnu)
  • LLRT LLRT v0.8.0-beta (linux, x64)
  • esrun esrun 0.29.0
How it's measured
  • Runtimes run interleaved, in randomized order each repetition — so they share one contention window and the ranking is fair, not decided by which ran first.

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

  • Cells with high run-to-run variance are flagged in the raw output.

Standings at a glance

Who placed where across every metric — and a quick, neutral take on where each runtime fits in production. Pick by fit, not by medal count.

RuntimeTests🥇🥈🥉4th5th
Bun67331310101
esrun6792514163
Deno65514211411
Node.js6689191713
LLRT501262822

Across 67 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 — parse cost and resolution cost are not the same thing. Memory is given twice: idle, and while a live working set is retained.

Cold start (near-empty script)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
4.2ms
12MB
Deno
14.4ms
28MB
esrun
8.4ms
24MB
LLRT
3.6ms
11MB
Node.js
17.1ms
41MB
Parse + run ~100 KB script
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
15.5ms
16MB
Deno
22.4ms
28MB
esrun
3.3ms
11MB
LLRT
10.9ms
14MB
Node.js
34.1ms
46MB
Load a 300-module graph
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
12.6ms
25MB
Deno
32.4ms~
32MB
esrun
3.3ms
11MB
LLRT
14.2ms
12MB
Node.js
83.4ms
61MB
Peak resident memory (idle)
Runtime
Performance (lower is better)
Bun
12MB
Deno
28MB
esrun
24MB
LLRT
11MB
Node.js
41MB
Peak resident memory (under load)
Runtime
Performance (lower is better)
Bun
145MB
Deno
123MB
esrun
106MB
LLRT
162MB
Node.js
136MB

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

Engine

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

Tight compute loop
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
108.2ms
24MB
Deno
233.7ms
39MB
esrun
192.1ms
36MB
LLRT
2002.2ms
11MB
Node.js
193.4ms
54MB
JSON parse/stringify
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
145.4ms
39MB
Deno
192.3ms
56MB
esrun
184.7ms
53MB
LLRT
636.2ms
11MB
Node.js
260.8ms
80MB
JSON (large documents)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
415.4ms
116MB
Deno
514.7ms
138MB
esrun
462.6ms
135MB
LLRT
1622.2ms
143MB
Node.js
667ms
159MB
Regex (match, validate, replace)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.5ms
35MB
Deno
62.6ms
40MB
esrun
58.1ms
39MB
LLRT
1168.3ms
11MB
Node.js
64.9ms
56MB
String building and slicing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
58.7ms
45MB
Deno
57.5ms
48MB
esrun
56.4ms
46MB
LLRT
147.3ms
11MB
Node.js
60.2ms
64MB
structuredClone
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
229.6ms
32MB
Deno
249.8ms
43MB
esrun
267.2ms
51MB
LLRT
310ms
12MB
Node.js
213.1ms
55MB
throw/catch + stack capture
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
308.1ms
48MB
Deno
4120.3ms
63MB
esrun
383.4ms
46MB
LLRT
304.6ms
11MB
Node.js
1410.4ms
64MB
async/await throughput
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
38.3ms
33MB
Deno
30.3ms
40MB
esrun
30.1ms
38MB
LLRT
666.5ms
11MB
Node.js
55.4ms
55MB
setTimeout churn
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
29.5ms
50MB
Deno
236.4ms
167MB
esrun
50.3ms
60MB
LLRT
36.7ms
50MB
Node.js
49.5ms
85MB

Web APIs

URL parsing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
30.5ms
50MB
Deno
96.3ms
47MB
esrun
84.6ms
46MB
LLRT
109.8ms
11MB
Node.js
33.7ms
63MB
URL setter
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
84.4ms
49MB
Deno
181.2ms
47MB
esrun
169.8ms
46MB
LLRT
109.3ms
11MB
Node.js
136ms
64MB
URLPattern test
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
475.4ms
116MB
Deno
5196.3ms
66MB
esrun
805.7ms
43MB
LLRT
Node.js
388.6ms
72MB
TextEncoder/TextDecoder (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.1ms
39MB
Deno
67ms
50MB
esrun
77.8ms
47MB
LLRT
70.4ms
11MB
Node.js
65.8ms
66MB
TextEncoder/TextDecoder (64 KiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
72.8ms
41MB
Deno
136.9ms
40MB
esrun
83.6ms
39MB
LLRT
213.1ms
14MB
Node.js
309.6ms
55MB
base64 (atob/btoa)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
11.1ms
38MB
Deno
7.7ms
42MB
esrun
21.7ms
37MB
LLRT
32.8ms
11MB
Node.js
6.9ms
55MB
TypedArray / DataView
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.1ms
35MB
Deno
12.7ms
41MB
esrun
12.6ms
39MB
LLRT
72.1ms
12MB
Node.js
13.2ms
56MB
Headers / Request parsing
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
224.4ms
41MB
Deno
1546.3ms
127MB
esrun
410.8ms
50MB
LLRT
726.4ms
12MB
Node.js
429.6ms
76MB
Multipart FormData round trip
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
20ms
47MB
Deno
366.5ms
111MB
esrun
91.9ms
43MB
LLRT
1361.6ms
13MB
Node.js
262.6ms
121MB
Date + Intl formatting
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
75.5ms
42MB
Deno
131.9ms
48MB
esrun
134ms
44MB
LLRT
Node.js
123.5ms
63MB
ReadableStream piping
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
5.7ms
27MB
Deno
10.7ms
43MB
esrun
10.2ms
43MB
LLRT
Node.js
11.1ms
61MB
CompressionStream round trip (gzip)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
27.9ms
42MB
Deno
200.6ms
46MB
esrun
66.7ms
51MB
LLRT
Node.js
296.6ms
73MB

Crypto

SubtleCrypto SHA-256
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
423ms
37MB
Deno
417.4ms
42MB
esrun
331.5ms
35MB
LLRT
329.1ms
11MB
Node.js
533.1ms
58MB
HMAC + AES-GCM
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
82.7ms
33MB
Deno
92.1ms~
42MB
esrun
33.5ms
36MB
LLRT
24.7ms
11MB
Node.js
213.2ms
63MB
ECDSA P-256 sign + verify
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
190.1ms
25MB
Deno
2209.9ms
41MB
esrun
1120.4ms
35MB
LLRT
1034.3ms
11MB
Node.js
410.6ms
58MB
PBKDF2 (10k iterations)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
63.1ms
18MB
Deno
72ms
34MB
esrun
101.1ms
28MB
LLRT
98.3ms
11MB
Node.js
69.9ms
50MB

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

Hashing

crypto above measures crypto.subtle — asynchronous, and returning bytes a caller still has to encode. These rows measure what that caller wanted: runtime:hashing for esrun, Bun.CryptoHasher for Bun, and node:crypto createHash for Node, Deno and LLRT. hash_fast has no standard answer in Node, Deno or LLRT, which is why they are n/a.

SHA-256 to hex (one-shot)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
175.5ms
37MB
Deno
180.5ms
56MB
esrun
312.9ms
39MB
LLRT
315.9ms
13MB
Node.js
160.2ms
60MB
SHA-256 over 64 KiB chunks (4 MiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
1652.1ms
24MB
Deno
1649.9ms
44MB
esrun
3063.2ms
31MB
LLRT
3044.7ms
13MB
Node.js
1465.7ms
54MB
Non-cryptographic hash (64 KiB)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
46.4ms
25MB
Deno
esrun
63.1ms
40MB
LLRT
Node.js
Where this leaves esrun, and why

hash_hex and sha256 are the same work — 20 000 × SHA-256 of the same 4 KiB buffer — one through async crypto.subtle, one through a synchronous call. esrun's two numbers are 6% apart (327 / 308), Node's are 3.2× apart (504 / 158): crypto.subtle here is the sync op with a resolved promise around it, so esrun wins the async row and loses the sync one, and neither is about the hash function. What is about the hash function is the remaining ~2× — RustCrypto's portable Rust against OpenSSL's hand-written AVX2 assembly, 277 MB/s to 580. The reference machine has no SHA-NI, so both sides run software SHA-256; sha2 selects an SHA-NI backend at runtime on hardware that has the instructions, which this box does not. hash_fast is what the module is actually for: 16× the data in a fifth of the time.

Network

HTTP server drives each runtime's own server from a client in the same process, so it measures the server and that runtime's fetch together, not the server alone. For server throughput on its own, bench/rps.sh uses an external load generator on separate cores.

fetch (local server)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
17.9ms
22MB
Deno
34.5ms
43MB
esrun
40.5ms
39MB
LLRT
17.7ms~
13MB
Node.js
95ms
90MB
fetch (streamed upload)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
39.9ms
25MB
Deno
36.4ms
44MB
esrun
38.9ms
39MB
LLRT
Node.js
104.5ms
88MB
HTTP server (concurrent)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
48ms
33MB
Deno
97.8ms
70MB
esrun
110.6ms
62MB
LLRT
Node.js
368.2ms
156MB
WebSocket round trips
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
405.6ms
36MB
Deno
536.2ms
47MB
esrun
743.4ms
41MB
LLRT
Node.js
593.3ms
74MB
UDP round trips (10 000 datagrams)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
180.9ms
36MB
Deno
401.1ms
68MB
esrun
313.9ms
40MB
LLRT
374.5ms
13MB
Node.js
270.9ms
61MB
UDP send throughput (50 000 datagrams)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
174.9ms
28MB
Deno
244.4ms
42MB
esrun
192.4ms
39MB
LLRT
681.9ms
13MB
Node.js
233.2ms
61MB

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

Hello world, measured externally

A framework hello-world (Hono) per runtime, driven by an external load generator on separate cores. LLRT has no general HTTP server, so it is not in this set.

Hono hello-world · Speed & Memory
Runtime
Throughput (higher ↑)
Memory (lower ↓)
Bun
80.0k
50 MB
Deno
79.0k
72 MB
esrun
53.7k
46 MB
Node.js
32.7k
128 MB

Burst vs sustained

The chart above is a burst against a freshly started process. Held under load instead, the heap is full and the collector has been running throughout.

RuntimeBurst req/s60s req/sChange
Bun80.0k83.1k+3.9%
Deno79.0k78.6k-0.5%
esrun53.7k54.2k+0.9%
Node.js32.7k33.6k+2.7%

Static files, measured externally

A 64 KiB file per response, driven by an external load generator on separate cores — the server alone, unlike the in-process row above. Bun and Deno hand a file handle to the kernel; Node streams it; esrun reads it into a buffer.

HTTP requests/sec · 64 KiB static file
Runtime
Throughput (higher ↑)
Memory (lower ↓)
Bun
26.5k
34 MB
Deno
14.1k
139 MB
esrun
15.7k
131 MB
Node.js
12.5k
280 MB

Filesystem

Page-cache resident, no fsync — this is 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
36.7ms
36MB
Deno
40ms
42MB
esrun
50.6ms
41MB
LLRT
26.7ms
12MB
Node.js
111.9ms
57MB
File read (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
16.4ms
37MB
Deno
66.7ms
167MB
esrun
36.7ms
95MB
LLRT
7.1ms
21MB
Node.js
58.7ms
114MB
File write (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
12.3ms
25MB
Deno
87.9ms
42MB
esrun
86.3ms
33MB
LLRT
98.8ms
12MB
Node.js
157.7ms
57MB
File write (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
20.7ms
24MB
Deno
40.4ms
81MB
esrun
36.5ms
33MB
LLRT
44ms~
20MB
Node.js
59.4ms
93MB
File append (small)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
32.2ms
26MB
Deno
42.7ms
42MB
esrun
40.2ms
33MB
LLRT
Node.js
95.8ms
57MB
File append (large)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
7.3ms
21MB
Deno
15.8ms
51MB
esrun
9.3ms~
27MB
LLRT
Node.js
25.9ms
61MB
File stat (one path)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
47.2ms~
27MB
Deno
92.7ms
41MB
esrun
66.8ms
34MB
LLRT
41.7ms
13MB
Node.js
70.5ms
51MB
File stat (1000 paths)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
200ms
36MB
Deno
372.3ms
45MB
esrun
285.9ms
40MB
LLRT
168.7ms~
12MB
Node.js
273.8ms
60MB
File exists (one path)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
7.5ms
28MB
Deno
90.2ms
41MB
esrun
52ms
33MB
LLRT
50.2ms~
13MB
Node.js
61.2ms
51MB
File exists (1000 paths)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
30.9ms
40MB
Deno
372ms
46MB
esrun
229.2ms
34MB
LLRT
195.5ms~
12MB
Node.js
243.5ms
61MB
Glob scan
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
28.1ms
34MB
Deno
esrun
50.5ms
35MB
LLRT
Node.js
190.5ms
67MB

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

System

Starting a child process and collecting its output — Deno.Command, Bun.spawn, node:child_process, and esrun's runtime:system Command.

Spawn a child process
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
135.4ms
27MB
Deno
153.9ms
52MB
esrun
88.8ms
35MB
LLRT
129.9ms
12MB
Node.js
255.5ms
55MB

Tracing and async context

Measured separately, by bench/probe-tracing.sh, and shown where the design is explained:

  • Async context — propagation cost against Node, Bun and Deno, and the promise-hook overhead that is a known gap.

  • Diagnostics — one op with nothing subscribed, with a subscription, and under --otel.

Behaviour under memory exhaustion

What happens when a script asks for more than the machine can give. The question is not speed but whether the guest survives: 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()gracefulgracefulexit 1gracefulgraceful

Native vs third-party parsers

Each runtime parses with the best facility it ships: esrun's runtime:serialization, Bun's native Bun.YAML and Bun.TOML, LLRT's llrt:xml. Node and Deno ship no parser for these formats, so they use the libraries you would reach for anyway (js-yaml, @iarna/toml, fast-xml-parser, msgpackr) — a native-vs-library gap on those rows, not a runtime-vs-runtime one.

JSONL streaming (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
849.5ms
86MB
Deno
665.6ms
146MB
esrun
567.2ms
71MB
LLRT
Node.js
619.7ms
150MB
XML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
484.1ms
87MB
Deno
418.1ms
72MB
esrun
154.2ms
33MB
LLRT
60ms
12MB
Node.js
478.8ms
80MB
XML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
910.9ms
90MB
Deno
875.7ms
206MB
esrun
309.4ms
82MB
LLRT
124.6ms
22MB
Node.js
938.6ms
278MB
YAML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
93.8ms
48MB
Deno
177.8ms
49MB
esrun
219ms
35MB
LLRT
4190.4ms
14MB
Node.js
191.1ms
63MB
YAML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
189.2ms
59MB
Deno
364.1ms
124MB
esrun
434.1ms
66MB
LLRT
8368ms
23MB
Node.js
384.3ms
140MB
TOML parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
52.9ms
52MB
Deno
223.4ms
61MB
esrun
156ms
34MB
LLRT
4066.7ms
15MB
Node.js
198.5ms
67MB
TOML parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
105ms
62MB
Deno
475.5ms
200MB
esrun
320.9ms
65MB
LLRT
8127.4ms
21MB
Node.js
390.7ms
212MB
MessagePack parsing (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
49.6ms
55MB
Deno
38.6ms
59MB
esrun
50ms
34MB
LLRT
1098.7ms
12MB
Node.js
40.8ms
62MB
MessagePack parsing (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
50.8ms
65MB
Deno
41.1ms
76MB
esrun
55.4ms
64MB
LLRT
1093.5ms
20MB
Node.js
41.5ms
82MB

Protobuf (each runtime's own path)

esrun decodes with its native runtime:serialization Protobuf (pure-JS, reflective); Node, Bun, and Deno decode with protobuf-es (@bufbuild/protobuf). Identical bytes, each runtime's real protobuf path.

Protobuf decode (small dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
74.5ms
60MB
Deno
71ms
52MB
esrun
74ms
44MB
LLRT
1059.6ms
19MB
Node.js
57.8ms
68MB
Protobuf decode (large dataset)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
345.8ms
167MB
Deno
406.7ms
248MB
esrun
390.6ms
220MB
LLRT
5371.4ms
88MB
Node.js
328.7ms
297MB

WebAssembly

esrun reaches V8's own WebAssembly implementation through the standard JS API, as Node and Deno do; Bun uses JavaScriptCore's. LLRT has no WebAssembly at all, so every cell here is n/a for it.

WebAssembly.compile (~250 KB module)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
63.1ms
167MB
Deno
36.9ms
221MB
esrun
39.3ms
222MB
LLRT
Node.js
45.1ms
319MB
JS↔wasm call boundary
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
142.8ms
31MB
Deno
78.7ms
42MB
esrun
79.5ms
39MB
LLRT
Node.js
102.4ms
56MB
wasm linear memory (shared buffer)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
365.5ms
31MB
Deno
239.7ms
44MB
esrun
250.9ms
39MB
LLRT
Node.js
204.1ms
56MB

The three measure different things. Compile is validation plus baseline codegen on a ~250 KB module, with a different salt each iteration so nothing is served from a code cache. The call boundary (20M calls into a trivial exported add) and linear memory (JS fills a 64 KiB window, wasm sums it back) are where real interop spends its time. esrun is level with Deno and ahead of Node and Bun on all three.

WASI

wasm32-wasip1, which is what a compiled CLI targets. Bootstrap is 2,000 x (construct a WASI, instantiate a command module, run _start) with compilation hoisted out, so it is the per-invocation cost of running a wasm program. Syscalls runs a guest whose _start loops 20,000 times over random_get and clock_time_get — the calls come from inside wasm, where a real program makes them, so it measures the host-side preview-1 implementation.

WASI bootstrap (instantiate + _start)
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
515.8ms
44MB
Deno
50.8ms
65MB
esrun
44.3ms
39MB
LLRT
Node.js
276.5ms
77MB
WASI syscalls from inside wasm
Runtime
Performance (lower is better)
Memory (lower ↓)
Bun
13.4ms
43MB
Deno
18ms
54MB
esrun
53.5ms
43MB
LLRT
Node.js
44.8ms
55MB

esrun's bootstrap is the fastest measured, a little ahead of Deno and several times quicker than Node or Bun. Its syscall path is not: Deno's is around 3x faster, because esrun implements preview-1 in JS rather than natively. Bun's number on that row is an outlier large enough to be worth checking against your own workload before trusting it either way.

WebSocket chat broadcast

C clients in one room; the server fans every message out to all of them. Cells are RECV messages/sec (fan-out), 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, so it's not in this sweep.

ServerC=32C=64C=128
Bun250,802249,207258,877
Deno209,930205,849215,295
esrun295,796111,08751,638

esrun serves through the capability-secured driven seam (each connection a spawned actor), not a native pub/sub server. A batched broadcast() leads at low fan-out; as C grows the central connection-map lock costs throughput while Bun/Deno hold flat.

Client throughput

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

ClientC=32C=64C=128
Bun247,822248,388248,798
Deno250,012260,502250,123
esrun253,512252,172265,192
Node.js254,593264,566255,640

See the WebSocket guide.

HTTP/1.1 vs HTTP/2

The same hello-world server driven twice — once over HTTP/1.1, once over cleartext HTTP/2 (h2c) — so the only variable is the protocol version. Cells are req/sec, best of 3 interleaved repetitions. Higher is better.

Unlike the charts above, this table is driven by an external load generator against each runtime's own server, and it is regenerated on its own (bench/http2.sh) rather than as part of the full sweep — so treat it as a separate run, not as another row of the same experiment.

RuntimeWide — 50 conns × 1 streamNarrow — 1 conn × 50 streams
HTTP/1.1HTTP/2GainHTTP/1.1HTTP/2Gain
Bun88,85838,6270.43×30,43650,1961.65×
Deno90,01821,8260.24×30,38837,5861.24×
esrun50,39240,4060.80×18,69766,3083.55×
Node.js32,18015,7880.49×22,22437,3011.68×

Two shapes, because an HTTP/2 number on its own says nothing — it is dominated by how the client connected:

  • Wide (50 connections, 1 stream each) is HTTP/1.1's best case: it already has 50 sockets, so there is nothing to multiplex and h2 is pure framing overhead. Every runtime loses here, esrun least of them.

  • Narrow (1 connection, 50 streams) is what HTTP/2 is for. HTTP/1.1 on one connection is strictly serial — the next request goes out only after the previous response comes back — while HTTP/2 carries all 50 at once. This is the shape a reverse proxy, an API gateway, or a gRPC client is in.

esrun leads the narrow HTTP/2 column outright, from the slowest HTTP/1.1 baseline on that shape: runtime:http hands responses back per request rather than per connection, so multiplexed streams could always complete out of order and the feature needed no new machinery to exploit.

Which comparisons this table licenses

Compare down a column freely — one client shape against each runtime's best available server. The gain column only isolates the protocol for rows without a †: Node and Bun serve cleartext h2 from node:http2 while their HTTP/1.1 number comes from node:http/Bun.serve, so their ratio also carries a change of implementation. And don't compare gains across runtimes — a weaker HTTP/1.1 baseline inflates a multiple, which is part of why esrun's is the largest.

See the HTTP/2 section of the HTTP guide.

Redis clients

esrun with @opentf/esrun-redis against Bun's built-in RedisClient and, on Node and Deno, both npm clients — redis (node-redis, the official one) and ioredis (roughly twice the downloads). Both are kept because they do not perform alike, and standing one in for the other would misreport Node.

Wall ms, min of 5, lower is better. Every workload prints a checksum the runner compares; all six columns agreed on all five.

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
listLRANGE 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

Bun's client leads everything, by 1.3× on the round trips and 3–5× where decoding dominates. It is native C++ against four JavaScript clients, and the gap is what that buys.

Among the JavaScript clients, esrun is fastest where the round trip dominates — 858 ms against node-redis's 1029 and ioredis's 910 — and does it in a third of the memory (38 MB against 79–124). It is also ahead of the official node-redis on the batch (225 vs 374) and the list scan (158 vs 203).

One workload is a genuine loss: repeated HGETALL at 957 ms against ~480 ms for both npm clients, twice as slow. That is not decoding in general — the list scan is mid-pack — it is specifically the map path.

What the map gap is, and what it is not

Not UTF-8 decoding: reading the same 50 000-element reply with { binary: true }, which skips every TextDecoder call, was 2% faster. Not startup either — esrun launches an empty script in 8 ms against Node's 20 and Deno's 24.

What is left is how a RESP3 map is represented on the way to the object a caller asked for: a pair array of wrapper objects, each holding a copied Uint8Array, before anything is turned into a key or a value. At 200 000 field/value pairs that is roughly four allocations each. ioredis negotiates RESP3 too and returns the same object twice as fast, so this is a fixable implementation cost rather than a cost of the protocol.

Unlike the rest of this page, these runs are not interleaved — each column is measured in turn — so treat small differences as noise and the order of magnitude as the finding. Reproduce with bench/db/redis/run.sh.

Reproduce

The harness lives in bench/. It auto-detects installed runtimes and can emit the JSON that powers 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

For CLI/script benchmarking, tools like hyperfine are also a good fit. Numbers vary by hardware; treat them as relative, not absolute.

Last updated on
Edit this page