Benchmarks
GioJS keeps memory flat under sustained load because Rust owns the HTTP layer - cache hits never allocate in Node. Self-hosted Next.js allocates in the Node event loop for every request, including cache hits.
Memory stability - GioJS vs Next.js 15
The table below shows illustrative, projected figures - not measurements. It sketches the expected pattern; run the benchmark scripts in benchmarks/memory-stability/ on your own hardware for real numbers (the harness uses 50 concurrent connections, 60 seconds, 3 runs per server, RSS sampled every 5 seconds, medians across runs).
| Time (s) | GioJS RSS (MB) | Next.js 15 RSS (MB) |
|---|---|---|
| 0 | 85.0 | 120.0 |
| 10 | 85.5 | 148.0 |
| 20 | 86.0 | 176.0 |
| 30 | 86.5 | 204.0 |
| 40 | 87.0 | 232.0 |
| 50 | 87.5 | 260.0 |
| 60 | 88.0 | 288.0 |
See benchmarks/memory-stability.md for the full methodology and for the table the harness populates with measured results.
Why GioJS stays flat
In self-hosted Next.js, the Node.js HTTP layer allocates a new buffer for every incoming request - even when the response is a cache hit. Under 50 req/s, GC pressure grows continuously and RSS climbs 2–5 MB per minute.
GioJS routes HTTP in Rust. A cache hit in the Rust layer is zero bytes allocated in Node - the response is served directly from the LRU without touching the V8 heap. Only cache misses cross the IPC boundary to Node for rendering.
Throughput
Cache-hit throughput (static pages) is bounded by Rust I/O, not Node. The figures sometimes quoted for this class of architecture (tens of thousands of cached requests/second at sub-millisecond p99) are projections, not GioJS measurements - benchmark on your own hardware before relying on specific numbers.
For dynamic pages (cache misses), throughput is similar - both are bounded by React render time.
Load testing with gio bench
gio bench ships with @gio.js/server: a zero-dependency HTTP load generator (plain node:http, keep-alive connections). It opens N concurrent connection loops for a fixed duration and reports requests/s, latency p50/p90/p99/max (nearest-rank, no sampling), non-200 count, errors, and bytes/s. Warmup requests are sent but excluded from all statistics.
gio bench <url> [--connections 32] [--duration 10] [--warmup 2]
gio bench --suite /,/posts/1 --base http://localhost:3000--connections- concurrent keep-alive connections (default 32)--duration- measured seconds per target (default 10)--warmup- unmeasured warmup seconds (default 2)--suite- comma-separated paths, run sequentially and printed as an aligned table--base- base URL that bare paths resolve against (defaulthttp://localhost:3000)
Each result includes the X-Gio-Cache value of the last response, so a cache-hit benchmark labels itself and cannot be silently confused with a cache-miss one. See benchmarks/README.md in the repository for the full methodology - including how to run an honest GioJS vs Next.js comparison and why localhost microbenchmarks must not be read as user-facing speedups.
Running benchmarks yourself
The benchmark infrastructure lives in benchmarks/memory-stability/:
run-benchmark.ps1- Windows PowerShell scriptrun-benchmark.sh- Linux/macOS bash scriptcollect.js- parses raw samples, computes medians, writes the markdown tablenext-baseline/- the Next.js 15 app used as a baseline