hyper.dev
well come

🖐🏽

2026-10-29 · letloop - 1 - micro-benchmarks don’t tell the whole story

I published a benchmark report comparing six HTTP server implementations across Rust, Scheme, JavaScript, and Python. Scheme (flow2 + io_uring) placed second at 306k req/s — behind Rust (axum) at 407k, level with Bun canary (303k) and Bun (294k), and ahead of Node.js and FastAPI.

The Scheme server now runs on flow2, letloop’s concurrency library, and on nothing beneath it: every read, write, timer, and accept goes through flow2’s public API.

These are micro-benchmarks. I want to be honest about what they show and what they don’t.

what the numbers say

A trivial counter endpoint. Single-threaded. Ten seconds per concurrency level. wrk as the load generator. No disk, no database, no parsing, no serialization. The purest measure of “how fast can this runtime shuffle bytes through a socket.”

Here’s how throughput scales with concurrent connections for four runtimes that represent four different async strategies:

         req/s (thousands) vs concurrent connections

  420 |
  400 |              R  R
  380 |                     R    R    R
  360 |                                    R
  340 |           R                             R  ← Rust (axum + tokio)
      |
  300 |                     S    S
  280 |                               S    S
      |
  240 |              S  S                       S  ← Scheme (flow2 + io_uring)
  220 |           S
  200 |                 N
  180 |           N  N      N    N    N
  160 |        R                           N    N  ← Node.js (stdlib)
  140 |        *
  120 |
      |
      |
   60 |
   40 |        F  F  F  F   F    F    F    F    F  ← FastAPI (uvicorn)
      |
    0 |
      +------------------------------------------→
               1  2  4  8  16   32   64  128  256
                    connections

      R Rust   S Scheme   N Node.js   F FastAPI   * overlap

Rust peaks early at 407k req/s (8 connections), then declines gently to 347k at 256 — the hallmark of a mature runtime with a well-tuned scheduler. Scheme peaks later at 306k req/s (16 connections), holds near 300k up to 64, then eases to 234k at 256. Node.js sits flat around 190k from 4 connections onward, sagging to 160k at 256. FastAPI never breaks 37k — the Python GIL is the ceiling, not uvicorn.

But the more interesting picture is latency under pressure:

         p99 latency (ms) at 256 concurrent connections (log scale)

  Rust       ▓▓▓▓▓  0.87
  Scheme     ▓▓▓▓▓▓▓  1.79
  Node.js    ▓▓▓▓▓▓▓▓  2.05
  FastAPI    ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  273.48

At 256 connections, Scheme’s p99 is 1.79ms — about twice Rust’s 0.87ms, and a little below Node.js’s 2.05ms. FastAPI’s p99 explodes to 273ms — a hundred and fifty times worse than Scheme. This is where architecture starts to show through the numbers.

what the numbers don’t say

Nothing about correctness. Nothing about how the code reads, how it changes, how it fails. Nothing about what happens when you add a database, a parser, a state machine, an authentication layer. Nothing about the six months after the benchmark, when the system has to evolve under production pressure and the original author is gone.

A micro-benchmark is a controlled experiment. It holds everything constant except the variable you’re measuring. That’s its virtue and its limit. The variable here is raw I/O throughput. Everything that makes a real system hard — concurrency bugs, resource leaks, error propagation, compositional complexity — is absent by design.

why it still matters

The benchmark exists to answer one narrow question: is the letloop async runtime fast enough that choosing Scheme doesn’t mean accepting a performance tax? The answer is yes. At 306k req/s with a p99 under 1.8ms at high concurrency, flow2 + io_uring is level with Bun, behind Rust, ahead of Node.js, and dramatically ahead of Python.

That matters because the choice was never about speed. It was about something else.

transparent async

Most async runtimes are colored. In Rust, you write async fn and enter a different world — different traits, different lifetimes, different debugging. In JavaScript, you write async/await and functions split into two kinds: the ones that return promises and the ones that don’t. In Python, the split is even worse — asyncio is essentially a parallel universe that doesn’t compose cleanly with synchronous code.

letloop’s async is transparent. You write ordinary Scheme. The runtime handles scheduling via io_uring underneath. There is no function coloring, no await keyword, no split between “async code” and “normal code.” A procedure that reads from a socket and a procedure that computes a factorial use the same call conventions, the same continuation model, the same debugging tools.

This is not a cosmetic difference. Function coloring has real costs:

It doubles the API surface — every library that does I/O needs sync and async variants, or forces callers into one world.

It makes composition harder — mixing sync and async code is where most concurrency bugs live in Python and JavaScript.

It raises the floor of understanding — a new developer can’t ignore the async/sync distinction; it’s load-bearing from day one.

Transparent async removes these costs. The entire system has one calling convention. Composition is ordinary function composition. The floor of understanding is lower. Not zero — io_uring itself is complex machinery — but the complexity stays below the API boundary where it belongs, instead of leaking into every function signature.

what’s next

chez scheme + pico http parser + io_uring is a work in progress. The scheduler is single-threaded. The error handling story is incomplete. These are real gaps, not aspirational roadmap items.

But the foundation is sound. The numbers confirm that the architecture doesn’t impose a performance ceiling. What matters now is everything the benchmark doesn’t measure: correctness, composability, comprehensibility. The things that make a system livable for the person who maintains it after the benchmarks are forgotten.

The full report with all six implementations is at hyper.dev/letloop/report-iii.html.