When you run a server on the edge, or one process per request in a serverless environment, the number that decides your worst case is not requests per second. Requests per second describes a server that is already up and warm. The number that governs your worst case is the cold moment before that: time to serve, how long from the moment a process is spawned until it answers its first request. That is the wait a user actually sits through when a fresh process has to come up and respond, and it is the number this benchmark measures, across Cruft, Node, and Bun.
The whole thing lives in cruft-verify, our public reproduction repository. Point it at the three runtimes and run one command:
node bench/scripts/run-cruftbench.mjs run --suite server-cold-start --engines node,bun,cruft
It finishes in about a minute.
What we measured
The quantity is simple to state: how long it takes to spawn a server process and receive its very first HTTP 200 in reply. The runner starts a server on a loopback port, polls it from the outside until a request comes back 200, records the elapsed time, and kills the process. The headline number is the floor, the fastest of many runs, so noise only ever pushes it up.
We measure two fixtures. In the first, all three runtimes run the same node:http server, an apples-to-apples comparison of one code path:
import http from "node:http";
const PORT = parseInt(process.env.SCS_PORT || "4310");
http.createServer((req, res) => {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
}).listen(PORT, "127.0.0.1");
In the second, each runtime runs its own native server, the one you would actually reach for. Cruft's is cruft:serve:
import { serve } from "cruft:serve";
const PORT = parseInt(process.env.SCS_PORT || "4310");
serve({ port: PORT, hostname: "127.0.0.1", fetch() { return new Response("ok"); } });
Bun's is Bun.serve:
const PORT = parseInt(process.env.SCS_PORT || "4310");
Bun.serve({ port: PORT, hostname: "127.0.0.1", fetch() { return new Response("ok"); } });
Node's native server is node:http, so on the native fixture it runs the same code as the shared one above. Every fixture only starts a server, with no in-process client.
We run each fixture in two states. Warm, with the binary already resident in the page cache, the way a cached container image comes up. And cold, with the page cache purged before every spawn, so the runtime and its libraries are read from disk the way they are on a fresh machine.
Why the fixtures are server only
The reason the fixtures never make their own request is that we want to time one thing: how long the server takes to answer. If a fixture called itself, the process would also have to spin up its HTTP client, and on a runtime that initializes lazily the first touch of that client can cost as much as the server does. That client time says nothing about how fast the server responds, but it lands in the same measurement and can even reverse the order of the results. So the request comes from the runner, an outside prober, and the process under test has one job: come up and respond.
The results
Disk warm, floor milliseconds to first response. Shorter bars are better.
Disk cold, with the page cache purged before every spawn:
Postmortem
Cruft beats Node by roughly three times in every case, warm or cold.
In the case of a warm start, the comparison with Bun depends on how you get there. On the shared node:http path, Cruft reaches first response in 18.5 ms against Bun's 28.2. The gap is Bun's node:http compatibility layer showing through: on its own native server Bun answers in 13.4 ms, so routing a request through node:http more than doubles its time. Cruft does not pay that penalty, because its node:http compatibility path is as fast as its native cruft:serve. Bun is faster on its native path, where Bun.serve answers in about 13 ms. When it comes to serving warm, Bun is currently faster.
Cold is where it changes. With the page cache purged, Cruft is the fastest JavaScript runtime to first response on both fixtures, ahead of Bun and well ahead of Node. Bun's warm advantage evaporates once the pages have to be faulted in from disk, that is, once the operating system has to read the program's code off storage instead of handing it over from memory it already had. Most likely this is because Cruft has a considerably smaller footprint. The cost of a cold start is dominated by how much of the executable the operating system has to pull off disk before the program can run, and Cruft ships the smallest single binary of the three JavaScript runtimes tested here.
Cruft is the smallest, so there is less of it to read off disk before it can answer a request, and it pays the smallest cold-start penalty.
There is still more work to be done
This whole benchmark should be taken with a grain of salt. It measures time to serve, not throughput, and right now those are very different numbers. What this test shows is that Cruft's floor is already lower than Bun's and Node's ceiling. Throughput is another matter.
Node and Bun both have mature optimizing JITs, because they vendor JavaScriptCore and V8, the two industry-standard engines. Cruft vendors neither. Cruft has a completely new JavaScript engine called Cruft Core, and it does have a JIT of its own. LeJIT specializes hot code with type promotion, inline caches, and on-stack replacement, and where it engages, on tight numeric and array loops, Cruft already runs about 1.65 times faster than Node and comes within reach of Bun. This job is not one of those. It is dominated by object allocation and string building, which LeJIT does not yet specialize, so Exegesis, the Cruft Core interpreter, runs it. On that kind of work an interpreter is no match for the man-years of JIT optimization that V8 and JavaScriptCore boast. Here are those results.
The highlight of this blog is the cold path: the first response and the wait before a process is warm at all. On that number, Cruft does quite well, and it will only get better.
Give it a try
Everything here is reproducible from the cruft-verify repository. Use the following two commands to run the warm and cold lanes:
# warm lane
node bench/scripts/run-cruftbench.mjs run --suite server-cold-start --engines node,bun,cruft
# cold lane, purges the page cache between spawns and needs root
sudo -v && node bench/scripts/run-server-cold-start.mjs --cold-cache --engines node,bun,cruft
Your numbers may differ from ours on your hardware. Open an issue if you are seeing something radically different, over at the Cruft GitHub repository.
