After our scriptc comparison, we tried another
TypeScript-to-native compiler: Perry.
We gave it the Hono app we use for our own compiler work, with the standard
@hono/node-server adapter, and asked three questions: does it run, how fast
is it, and does it answer like Node?
It runs, though not from the npm release: that package couldn't link or start our apps (details below), so we built Perry's compiler and runtime from the same source tag. We changed no application or dependency code. The build reported 30 native modules and no JavaScript fallback modules.
Versions: Perry 0.5.1520, Node v24.21.0, Hono 4.12.34, @hono/node-server 2.1.1,
measured on September 23, 2026.
Perry in the wider comparison
This is the chart from our C++, Rust and
Gea post with Perry added; the earlier rows are unchanged. Bars show the mean of three
GET / samples, so they differ slightly from the medians in the Hono table below.
Raw HTTP and Hono are separate rows because they do different amounts of work. Perry's raw
row is the unchanged apps/raw-http-hello/server.ts from the earlier post, run
against Node on the same machine with the same pinning, 64 connections and three
eight-second samples per endpoint. Both returned the expected status, content type and body,
but Perry's headers and framing differ, so the earlier chart's “same bytes on the wire”
condition does not hold for the Perry rows.
Hono on one CPU
Both runtimes run the same server.ts. The timed endpoints return
Hello Hono! and {"hello":"world"}. Before timing, we checked those
two responses, a 404 and a JSON POST handler for status, body and content type. Both passed.
Median p99 latency was 51.89 ms for Perry and 2.38 ms for Node on /, and
62.92 ms and 3.39 ms on /json. All 12 samples finished with no socket errors
and no non-2xx/3xx responses.
Resident memory after each sample was 136.6–163.5 MiB for Perry and 112.0–114.7 MiB for Node (snapshots, not peaks). Perry's Hono executable is 25.9 MB.
Where the CPU time goes
Under load, both of Perry's binaries used a full CPU. The raw server averaged 44,509 requests per second; Hono averaged 2,795. Hono and its adapter cost about a factor of sixteen.
The Hono profile is dominated by Perry's JavaScript runtime machinery. About 48% of user-space cycles passed through its generic method dispatcher, 20% through promise microtask processing and 11% through minor garbage collection. These are inclusive call-stack figures, so they overlap and don't add up.
So the compiled binary still does dynamic dispatch, property lookup, promise scheduling and allocation on every request. The profile shows that this is where the time goes; it doesn't say how much of the gap to Node any one fix would close.
Perry with four workers
With a node:cluster launcher around the same handlers, Perry's raw server
averaged 84,351 requests per second on / and Hono averaged 6,533. All twelve
samples finished without socket or status errors.
What “1 out of 38” means
The compatibility battery is a separate raw node:http app,
not 38 Hono routes. The driver sends the same requests to Node and to the compiled binary,
including HEAD, HTTP/1.0, chunked uploads, pipelining, duplicate headers and malformed
input, and compares the full response bytes with only the Date value normalized.
Perry matched once, on the 204 response. That score is stricter than it sounds. On a plain
GET, Node sends a lowercase content-type and a chunked body; Perry sends
Content-Type and a Content-Length. Both return
Hello, World! GET /. The bytes differ; the answer doesn't.
The 36 doesn't mean 36 cases are fully compatible. Some differences live in headers, which
that comparison ignores: HTTP/1.0 keep-alive handling, implicit HEAD content lengths, and
two X-Extra fields merged into e1,e2.
Two requests got a different answer
In two cases the status and body both differed. Perry accepted a request that Node rejects each time.
These matter more than header casing. The test shows what the whole stack returned; it doesn't pin down which layer is responsible, and we didn't look for an exploit.
Getting a working build
We started with @perryts/perry@0.5.1520 from npm. The raw HTTP app failed to
link because libperry_ext_http.a was missing. Hono built, but the binary
exited before opening its port with
TypeError: Cannot read properties of undefined (reading 'listen').
We built the runtime libraries from the upstream v0.5.1520 tag. The npm
compiler rejected them: same version number, different build identity
(source 85ccd72f8e53 for the compiler, commit 381045a8735f for
the libraries).
Building the compiler from the same tag fixed that. We left the identity check on, used
the perry-dev profile with dev-cli, and let Perry build the
runtime features each app needed. That profile only affects how the compiler itself is
built; emitted programs still get normal LLVM optimization and the release runtime
archives. We also had to install LLVM's libpolly-22-dev.
The first successful Hono build took 435.52 seconds, including the full runtime build, so it isn't a warm compile time. Every throughput number here comes from this source build, with nothing else compiling during measurement.
Whose HTTP implementation is running?
The Node adapter calls node:http, and what that means depends on the runtime.
In Perry it reaches a Rust compatibility layer on Hyper and Tokio, which provides the
Node-facing API and calls back into the compiled app. See
Perry's HTTP server source.
Gea also brings its own. Our node:http is TypeScript compiled by geatsc on top
of a native C++ parser and reactor. We compile the real Hono package and its adapter, but
not Node's own HTTP implementation. The name describes the API, not where
the code came from.
Gea matched all 38 requests in an earlier run; we didn't rerun it here, and its chart rows come from that run. Passing this battery isn't proof of full Node compatibility, and our HTTP implementation lists its remaining deviations.
How we measured
The bench box is an Intel Xeon E3-1231 v3 at 3.40 GHz: four cores, eight logical CPUs.
For single-worker runs, each server and its threads were pinned to CPU 0. The load
generator, wrk -t4 -c64 over loopback, ran on logical CPUs 4–7. With SMT on,
that is logical separation, not guaranteed physical-core isolation.
Each run had a two-second warmup and an eight-second sample per endpoint. We ran three rounds and reversed server and endpoint order in the second. The Hono table uses medians; the chart uses means to match the original. We didn't test TLS, multipart uploads, database work or sustained production traffic.
For four workers, a node:cluster launcher wraps the same raw and Hono handlers,
with Hono still on @hono/node-server. Perry's default round-robin scheduling
hands connections to four workers. The primary and workers run on CPUs 0–3 and the load
generator on 4–7, as in the original comparison. Before each run we traced all 64
connections to their workers through socket descriptors (16 each) and checked that all
four workers stayed alive through timing.
Perry runs this unchanged Hono app. On one CPU the tested build served about an eleventh of Node's requests, and it answers differently at the edges. Compiling, answering correctly and running fast are separate questions, and here each got a different answer.