Looking for the direct Node vs Perry comparison? See benchmarks-perry-vs-node.md — identical source, two runtimes, same machine, same harness. The numbers in this file are the deployed CMS itself (Node + tsx) and are an end-to-end view including DB, fetch composition, and the customer site stack.
Two environments measured: local Mac (dev) + the live beta.perryts.com
(VPS). Both runs use Node + tsx (the Node-fallback path) since the
Perry-compiled native binary blocker isn't fixed yet — these are the
lower bound of what the CMS can do.
Tool: autocannon v8, 15 s per run, 10 concurrent connections, 3 s warm-up.
Reproduce: ./scripts/bench.sh local|remote|both.
| RPS | avg latency | p99 | |
|---|---|---|---|
/healthz (local) |
24,428 | <1 ms | <1 ms |
Cached API read (e.g. /api/v1/types) |
12,793 | <1 ms | <1 ms |
| Content list (20 rows, JSON) | 5,665 | 1.3 ms | 5 ms |
| Big content row (full markdown body) | 5,573 | 1.4 ms | 7 ms |
| Full settings dump (≈1.4 MB i18n) | 766 | 12.6 ms | 25 ms |
| Customer site home (13 sections, multi-fetch) | 1,077 | 8.8 ms | 21 ms |
| Customer site blog post (markdown render) | 1,302 | 7.2 ms | 15 ms |
| Customer site showcase/pry (rich repeater) | 3,850 | 2.1 ms | 5 ms |
| Sitemap (XML) | 17,106 | <1 ms | 1 ms |
Memory: CMS RSS ≈ 412 MB, customer site RSS ≈ 316 MB (both Node + tsx; native-binary path will land both well under 50 MB).
Caveats:
<1msshows where autocannon's per-percentile histogram has 1 ms resolution; theavg latencycolumn (with decimals) is the truthful number for sub-millisecond endpoints.- Both targets serve via tsx — no minification, no native binary yet.
- The customer site does multiple CMS reads per page (header menu, footer menu, locale bundle, content row, settings) — the home-page number is the worst case for "real" pages.
- Beta server is single ~2 vCPU VPS, ~30 ms RTT from the Mac running the bench.
| Endpoint | RPS | avg | p50 | p99 | max | bytes/s |
|---|---|---|---|---|---|---|
GET /healthz |
24,428 | 0.01 ms | <1 ms | <1 ms | 37 ms | 4.6 MB/s |
GET /api/v1/types |
12,793 | 0.04 ms | <1 ms | <1 ms | 17 ms | 138 MB/s |
GET /api/v1/menus/main?locale=en |
22,418 | 0.02 ms | <1 ms | <1 ms | 21 ms | 27 MB/s |
GET /api/v1/settings/site.locales |
22,261 | 0.02 ms | <1 ms | <1 ms | 10 ms | 9 MB/s |
GET /api/v1/content?type=post&locale=en&limit=20 |
5,665 | 1.30 ms | 1 ms | 5 ms | 21 ms | 1,128 MB/s |
GET /api/v1/content/by-slug/post/closing-the-gc-gap |
5,573 | 1.39 ms | 1 ms | 7 ms | 51 ms | 89 MB/s |
GET /api/v1/settings (full dump, ≈1.4 MB JSON) |
766 | 12.64 ms | 11 ms | 25 ms | 67 ms | 686 MB/s |
| Endpoint | RPS | avg | p50 | p99 | max | bytes/s |
|---|---|---|---|---|---|---|
GET / (13 sections, 5 CMS reads per render) |
1,077 | 8.78 ms | 8 ms | 21 ms | 72 ms | 68 MB/s |
GET /blog (index, 17 posts listed) |
3,792 | 2.32 ms | 2 ms | 5 ms | 24 ms | 77 MB/s |
GET /blog/closing-the-gc-gap (markdown render) |
1,302 | 7.18 ms | 6 ms | 15 ms | 42 ms | 37 MB/s |
GET /showcase/pry (rich repeater render) |
3,850 | 2.12 ms | 2 ms | 5 ms | 16 ms | 62 MB/s |
GET /compare/bun (compare-table render) |
3,094 | 2.73 ms | 2 ms | 8 ms | 57 ms | 57 MB/s |
GET /pricing (CMS-driven tiers + 4 FAQs) |
3,352 | 2.45 ms | 2 ms | 6 ms | 16 ms | 71 MB/s |
GET /sitemap.xml |
17,106 | 0.06 ms | <1 ms | 1 ms | 15 ms | 58 MB/s |
| Process | RSS |
|---|---|
beta-cms (Hono + tsx) |
412 MB |
beta-perryts (Hono + tsx) |
316 MB |
Includes transatlantic network (~30 ms RTT), TLS handshake amortized across keep-alive, nginx in front. Numbers are end-to-end including those layers.
| Endpoint | RPS | avg | p50 | p99 | max |
|---|---|---|---|---|---|
GET /healthz |
284 | 35 ms | 33 ms | 59 ms | 127 ms |
GET / (home) |
43 | 229 ms | 221 ms | 382 ms | 427 ms |
GET /blog |
129 | 77 ms | 73 ms | 149 ms | 197 ms |
GET /blog/closing-the-gc-gap |
92 | 108 ms | 103 ms | 192 ms | 825 ms |
GET /showcase/pry |
150 | 66 ms | 63 ms | 123 ms | 199 ms |
GET /pricing |
123 | 81 ms | 77 ms | 153 ms | 560 ms |
GET /sitemap.xml |
244 | 40 ms | 39 ms | 65 ms | 142 ms |
Note: home is the worst case because the layout fetches both menus +
the locale bundle in parallel before the render starts; smaller pages
like /showcase/pry only need one content row + cached chrome.
- Cached API reads sit at ~22 k RPS sub-millisecond on the dev box. The in-memory SDK cache + dep-graph invalidation means most reads on steady-state hit the cache directly.
- The full-settings dump is the obvious bottleneck at 766 RPS — it serializes the entire i18n × 13 locales bundle on every call (≈1.4 MB). Realistic apps don't fetch all settings on every request; they pull the keys they need.
- Customer-site home at 1 k RPS / 8.8 ms is the worst-case "user visits the site" path: layout fetches both menus + locale bundle in parallel, home renders 13 sections, each section pulls its t() calls. Single-page render around 1 ms.
- Customer-site rich pages (
/showcase/prywith 12 repeater cards/compare/bunwith a 10-row table) sit at ~3–4 k RPS / 2 ms, comfortably under a high-traffic blog's read pressure.
- No errors under any of the runs. Tail (
max) is occasional GC pauses up to ~70 ms; p99 ≤ 25 ms throughout.
These numbers are with:
- The Node-fallback path — Perry-compiled native binary should land 2–4× higher RPS + ~50 MB RSS (gscmaster's similar shape benchmarks in that range).
- Single process (no cluster/pm2 fork mode).
- tsx transpiling TypeScript on import — built JS would shave a bit.
So the headline numbers are conservative.
./scripts/bench.sh local|remote|bothreproduces this run.DURATION=15 CONNS=10 WARMUP=3was used here. IncreasingCONNSpast 10 pegs the dev-box CPU and the tail latency stops being meaningful for a localhost run.- The site fetches from CMS over localhost — there's no inter-host network hop on the local run.
<1msindicates autocannon's integer-ms percentile bucket; theavgcolumn has the actual sub-millisecond mean.- This is read-throughput only. Write benchmarks (publish, content create) are a follow-up — different shape, different limits (MySQL insert + cache invalidation + webhook dispatch).