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.