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.3.12
  • Deno deno 2.8.3 (stable, release, x86_64-unknown-linux-gnu)
  • LLRT LLRT v0.8.0-beta (linux, x64)
  • esrun esrun 0.9.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
esrun441217591
Bun441481741
Node.js443129164
Deno435713135
LLRT2610··115

Across 44 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) and peak resident set on a near-empty script.

Cold start (near-empty script)lower is better
esrun
7.1ms / 20MB
Bun
9.2ms / 30MB
Node.js
17.4ms / 41MB
Deno
24ms / 54MB
LLRT
3.3ms / 11MB
Parse + run ~100 KB scriptlower is better
esrun
18.2ms / 21MB
Bun
20.7ms / 33MB
Node.js
29.2ms / 40MB
Deno
32.4ms / 55MB
LLRT
11.2ms / 11MB
Peak resident memorylower is better
esrun
20MB
Bun
30MB
Node.js
41MB
Deno
54MB
LLRT
11MB

Standard Workloads

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

WebCrypto (sign/verify)lower is better
esrun
32.3ms / 32MB
Bun
83.6ms / 45MB
Node.js
188.9ms / 56MB
Deno
140.3ms / 67MB
LLRT
24.9ms / 11MB
SubtleCrypto SHA-256lower is better
esrun
340.1ms / 29MB
Bun
456.9ms / 48MB
Node.js
574.1ms / 56MB
Deno
501.2ms / 69MB
LLRT
335.2ms / 11MB
JSON parse/stringifylower is better
esrun
190.8ms / 48MB
Bun
203.1ms / 93MB
Node.js
278.2ms / 79MB
Deno
205.3ms / 79MB
LLRT
652.2ms / 11MB
JSON (large documents)lower is better
esrun
598.2ms / 153MB
Bun
619.5ms / 163MB
Node.js
681ms / 157MB
Deno
536.1ms / 162MB
LLRT
1865.1ms / 139MB
URL parsinglower is better
esrun
87.1ms / 41MB
Bun
76.3ms / 94MB
Node.js
48.4ms / 63MB
Deno
101.6ms / 73MB
LLRT
113.1ms / 11MB
URL setterlower is better
esrun
246.6ms / 41MB
Bun
279.7ms / 68MB
Node.js
126.9ms / 63MB
Deno
193.8ms / 72MB
LLRT
111.4ms / 11MB
URLPattern testlower is better
esrun
163.4ms / 37MB
Bun
723.8ms / 230MB
Node.js
414.9ms / 70MB
Deno
4937.4ms / 88MB
LLRT
TextEncoder/TextDecoderlower is better
esrun
70.3ms / 42MB
Bun
23ms / 64MB
Node.js
69.7ms / 64MB
Deno
70ms / 76MB
LLRT
73.1ms / 11MB
base64 (atob/btoa)lower is better
esrun
29.4ms / 32MB
Bun
14.2ms / 65MB
Node.js
7.1ms / 53MB
Deno
7.5ms / 66MB
LLRT
32.9ms / 11MB
structuredClonelower is better
esrun
311.5ms / 35MB
Bun
262.4ms / 88MB
Node.js
223.4ms / 54MB
Deno
262.3ms / 68MB
LLRT
326.4ms / 12MB
Tight compute looplower is better
esrun
236.1ms / 31MB
Bun
110.5ms / 40MB
Node.js
194.2ms / 53MB
Deno
211.5ms / 64MB
LLRT
2027.2ms / 11MB
async/await throughputlower is better
esrun
29.6ms / 33MB
Bun
49.3ms / 73MB
Node.js
58.8ms / 53MB
Deno
32.6ms / 66MB
LLRT
682.3ms / 11MB
setTimeout churnlower is better
esrun
5.2ms / 32MB
Bun
8ms / 38MB
Node.js
6.1ms / 60MB
Deno
23.9ms / 80MB
LLRT
3.8ms / 18MB
ReadableStream pipinglower is better
esrun
9.6ms / 37MB
Bun
20.3ms / 77MB
Node.js
22.7ms / 59MB
Deno
14.6ms / 70MB
LLRT
fetch (local server)lower is better
esrun
37ms / 32MB
Bun
19ms / 41MB
Node.js
88.5ms / 87MB
Deno
34.3ms / 64MB
LLRT
18.3ms / 13MB
fetch (streamed upload)lower is better
esrun
36.5ms / 31MB
Bun
45.5ms / 50MB
Node.js
109.8ms / 88MB
Deno
35.1ms / 62MB
LLRT
HTTP server (concurrent)lower is better
esrun
92.2ms / 46MB
Bun
67.9ms / 48MB
Node.js
396.7ms / 154MB
Deno
110.3ms / 86MB
LLRT
File read (small)lower is better
esrun
45ms / 37MB
Bun
34.4ms / 46MB
Node.js
125ms / 56MB
Deno
41ms / 64MB
LLRT
File read (large)lower is better
esrun
0.6ms / 89MB
Bun
8.2ms / 63MB
Node.js
21.2ms / 94MB
Deno
23.6ms / 109MB
LLRT
File write (small)lower is better
esrun
78.7ms / 28MB
Bun
12.3ms / 41MB
Node.js
182.5ms / 56MB
Deno
87ms / 64MB
LLRT
File write (large)lower is better
esrun
21.1ms / 27MB
Bun
18.1ms / 42MB
Node.js
56.7ms / 92MB
Deno
40ms / 103MB
LLRT
File append (small)lower is better
esrun
33.4ms / 28MB
Bun
34.2ms / 41MB
Node.js
107.7ms / 56MB
Deno
41.6ms / 64MB
LLRT
File append (large)lower is better
esrun
14.5ms / 37MB
Bun
19.8ms / 42MB
Node.js
47.1ms / 93MB
Deno
30.9ms / 103MB
LLRT
File stat (small)lower is better
esrun
74ms / 29MB
Bun
54.4ms / 43MB
Node.js
72.7ms / 48MB
Deno
95.9ms / 62MB
LLRT
File stat (large)lower is better
esrun
0.3ms / 26MB
Bun
0.2ms / 42MB
Node.js
1.1ms / 52MB
Deno
0.8ms / 64MB
LLRT
File exists (small)lower is better
esrun
58ms / 28MB
Bun
56.9ms / 45MB
Node.js
74ms / 50MB
Deno
95.7ms / 62MB
LLRT
File exists (large)lower is better
esrun
0.2ms / 26MB
Bun
0.2ms / 42MB
Node.js
0.5ms / 52MB
Deno
0.8ms / 64MB
LLRT
Glob scanlower is better
esrun
51.5ms / 30MB
Bun
32.6ms / 55MB
Node.js
215.8ms / 68MB
Deno
LLRT

Native vs Third-Party Parsers

Comparing runtimes running heavily optimized native parsing engines (esrun's runtime:serialization, LLRT's llrt:xml) versus JS-based fallback libraries (fast-xml-parser) for runtimes that lack native extensions (Node.js, Bun, Deno).

JSONL streaming (large dataset)lower is better
esrun
562.9ms / 62MB
Bun
748.7ms / 163MB
Node.js
635ms / 214MB
Deno
655.4ms / 200MB
LLRT
XML parsing (small dataset)lower is better
esrun
160.9ms / 28MB
Bun
498ms / 145MB
Node.js
507.9ms / 77MB
Deno
516ms / 89MB
LLRT
61.4ms / 12MB
XML parsing (large dataset)lower is better
esrun
339ms / 62MB
Bun
914.6ms / 229MB
Node.js
991.7ms / 282MB
Deno
988.3ms / 228MB
LLRT
125.4ms / 22MB
YAML parsing (small dataset)lower is better
esrun
241.1ms / 27MB
Bun
201.1ms / 109MB
Node.js
196.5ms / 61MB
Deno
182.6ms / 69MB
LLRT
4429ms / 11MB
YAML parsing (large dataset)lower is better
esrun
469.6ms / 57MB
Bun
368.2ms / 133MB
Node.js
382.8ms / 138MB
Deno
385.1ms / 145MB
LLRT
9009.1ms / 17MB
TOML parsing (small dataset)lower is better
esrun
165ms / 27MB
Bun
158.1ms / 94MB
Node.js
210.4ms / 65MB
Deno
215.6ms / 75MB
LLRT
4296.8ms / 11MB
TOML parsing (large dataset)lower is better
esrun
337.9ms / 60MB
Bun
346.4ms / 145MB
Node.js
419.5ms / 208MB
Deno
446.5ms / 215MB
LLRT
8413.4ms / 15MB
MessagePack parsing (small dataset)lower is better
esrun
49.6ms / 27MB
Bun
63.2ms / 102MB
Node.js
43.3ms / 58MB
Deno
40.3ms / 71MB
LLRT
1118ms / 11MB
MessagePack parsing (large dataset)lower is better
esrun
53.2ms / 57MB
Bun
56.8ms / 105MB
Node.js
43.4ms / 78MB
Deno
41.5ms / 91MB
LLRT
1137.1ms / 15MB

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)lower is better
esrun
76.6ms / 37MB
Bun
141.3ms / 118MB
Node.js
120.9ms / 66MB
Deno
186.6ms / 73MB
LLRT
1735.5ms / 12MB
Protobuf decode (large dataset)lower is better
esrun
4144.3ms / 108MB
Bun
6382.5ms / 223MB
Node.js
5879.9ms / 426MB
Deno
9507.1ms / 351MB
LLRT

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
esrun277,966114,88449,956
Bun209,342213,263218,167
Deno180,226182,714182,129

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
esrun218,485223,241222,020
Bun211,353216,538216,162
Deno215,337221,214218,723
Node.js220,930223,566223,664

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
esrun66,93953,0800.79×20,15773,5413.65×
Bun119,78543,0860.36×31,10949,1421.58×
Deno115,14127,4090.24×32,30339,2091.21×
Node.js36,59718,4130.50×23,22139,7001.71×

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.

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

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

# 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