All rise. The Honorable HttpArena presiding.
Today's docket is exclusively C# versus C#. No Rust defendants smirking from the gallery, no Go gophers passing notes. Just .NET frameworks, their press releases, and a 64-core AMD Threadripper PRO 3995WX that does not care about your README. Every entry was run through 27 scored profiles in an identical Dockerized harness — server pinned to one half of the chip, the gcannon load generator pinned to the symmetric other half, so the prosecution literally cannot steal cycles from the witness.
The court's motto, etched above the bench: "Don't just say you're fast, prove it."
Before any number is admitted into evidence, the defendant must pass an 18-point validation suite with randomized anti-cheat probes — random query sums, two different random POST bodies (to catch response caching), empty-range DB queries that must return zero rows, and a TCP fragmentation test that splits the request across multiple writes with 30ms pauses. You cannot hardcode your way to an acquittal. Correctness is a precondition for a score.
Let's hear the cases.
Count One: GenHTTP v. ASP.NET Core, the headline rivalry
The prosecution alleges that GenHTTP — a community project by Kaliumhexacyanoferrat — claims to outrun Microsoft's flagship. Bold. The kind of claim that usually gets a framework held in contempt.
Except the evidence holds up. Taking the best variant of each camp per profile, the tally is GenHTTP 14, ASP.NET 4.
Exhibit A: the CRUD podium. A clean C# sweep, top to bottom, in requests per second:
| Rank | Framework | RPS |
|---|---|---|
| 1 | genhttp-11 | 518,433 |
| 2 | genhttp | 464,774 |
| 3 | genhttp-kestrel | 452,652 |
| 4 | simplew | 430,132 |
| 5 | simplew-tuned | 417,396 |
| 6 | aspnet-minimal | 396,825 |
| 7 | aspnet-iouring | 357,713 |
The defense for Microsoft objects: "Minimal API is production-hardened, Your Honor." Sustained, and irrelevant. On this count, genhttp-11 beat aspnet-minimal by 1.31x.
The court asks: how? The answer is architectural, not magical. GenHTTP is a server and a framework in one library — your app is a tree of composable handler modules, not middleware on someone else's pipeline. On genhttp-11's native HTTP/1.1 engine, a request goes from a tight 1.1 parser straight into a precompiled handler tree with no ASP.NET middleware chain in the way. When the work per request is trivial — a baseline sum, a small JSON body — framework overhead dominates, and the leaner server wins.
// Wired once at startup, not per request:
Layout.Create()
.Add("pipeline", Content.From(Resource.FromString("ok")))
.AddService<Baseline>("baseline11")
.AddService<Json>("json")
.AddStaticFiles()
.AddWebsocket();
The allocation discipline is admitted into the record too: ArrayPool<byte>.Shared for the 16KB upload buffer, Websocket.Imperative().DoNotAllocateFrameData() for zero-copy echo frames, server GC, and an Npgsql multiplexing pool. This is not a framework that says it's careful. The bytes don't lie.
The full spread of GenHTTP's 14 wins, for the record: baseline 1.20x (2,075,459 vs 1,733,724), pipelined 1.40x (17,885,664 vs 12,807,538), limited-conn a brutal 2.24x, json 1.38x, json-comp 1.28x, json-tls 1.30x, upload 1.91x, static 1.47x, crud 1.31x, api-4 1.11x, api-16 1.29x, echo-ws 1.26x, and echo-ws-pipeline 1.45x (28.6M vs 19.7M frames). The fourteenth is baseline-h3 at 1.02x — the framework equivalent of winning an argument by a single word and then refusing to make eye contact. On plaintext HTTP/1.1 and WebSockets, the verdict is not close.
Count Two: ASP.NET's airtight alibi
Now the defense gets its moment, and it's a strong one. ASP.NET's 4 wins span HTTP/2 (baseline-h2, static-h2), HTTP/3 (static-h3), and async-db.
| Profile | Winner | Margin |
|---|---|---|
| baseline-h2 | aspnet-minimal | 1.48x (2,648,217 vs 1,792,309) |
| static-h2 | aspnet-minimal | 2.33x (375,583 vs 161,502) |
| static-h3 | aspnet-minimal | 1.20x |
| async-db | aspnet-minimal | 1.15x (225,491 vs 196,599) |
Here's the twist the jury needs to understand: GenHTTP's native engine is HTTP/1.1-only. No HPACK, no stream multiplexing, no QUIC. So for HTTP/2 and HTTP/3, GenHTTP must borrow Kestrel — the genhttp-kestrel variant ships libmsquic and binds 8443 over TCP and UDP. And once both sides are standing on Kestrel, Microsoft's years-hardened h2 stack — tuned here to 256 streams per connection, a 2MB connection window, 1MB stream windows — wins decisively. The 2.33x static-h2 margin isn't an upset. It's the incumbent defending its own courthouse.
That "Kestrel owns h2" story covers exactly two of these rows — baseline-h2 and static-h2. The other two wins are different animals. async-db is a database-bound profile; that one rides Npgsql and async query handling, not the maturity of an h2 stack. And static-h3 is HTTP/3 — where ASP.NET does not sweep, because GenHTTP took baseline-h3 by 1.02x. Call QUIC a split decision, not a shutout.
Engine/handler separation is the whole trick, and it cuts both ways: the same handler tree drops onto GenHTTP's engine or onto Kestrel unchanged. GenHTTP wins where its lean engine runs; it concedes where it has to rent Microsoft's.
Count Three: the witness who built their own runtime
aspnet-minimal-iouring takes the stand wearing the most elaborate alibi in the building. To swap Kestrel's socket transport from epoll to io_uring, it ships a hand-built fork of the .NET runtime (Ben Adams' branch, dotnet/runtime#124374) via a 3-stage Docker build that clones, patches socket internals with a Python script, and overlays System.Net.Sockets.dll onto the stock framework.
That's not a fighter, that's a fighter who reforged his own gloves in the locker room. The patch exists because the branch wouldn't even compile — it had to rewrite a &result taken on an out int param and clamp io_uring's 128-byte accept sockaddr into a family-sized buffer. The court admires the dedication. It also notes that io_uring with SQPOLL pays off on cheap pipelined h1, not CPU-bound h2/TLS — and on CRUD it landed 7th. Effort is not throughput.
Count Four: the marketing department, on trial
Some claims did not survive cross-examination.
ServiceStack, the "one framework to power them all," posted 22,976 RPS on json-4096 (dead last) and 14,268 on pipelined-512 (also dead last). The prosecution produces CPU traces: ~700–1700% utilization on a 64-core box. That is not a framework saturating its cores. That is the enterprise tax showing up on a Tuesday — a full DTO pipeline on Kestrel plus a global response-compression middleware that taxes even the plaintext ok route. Guilty of layering.
aspnet-mvc is its co-defendant. The same handlers that fly under Minimal API get bogged down behind controller dispatch, model binding, filters, content negotiation, and reflection-based JSON of anonymous objects on every single request. Minimal APIs compile each MapGet into a near-direct RequestDelegate; MVC pays the family's catering bill, request after request.
aspnet-minimal-aot pleads efficiency. True — Native AOT strips the JIT and reflection surface and wins on cold start. But under cross it concedes ~900MiB–1GiB working set, because ServerGC reserves the heap regardless. Footprint is a heap problem, not a JIT problem.
simplew-tuned is the most honest witness in the room: the entire speedup over plain simplew is four socket flags — TcpNoDelay, ReuseAddress, TcpKeepAlive, ReusePort — plus a compressed static-file cache. Same application code. No perjury, just config.
A note on the geomean defendants
The court must address an attempted mistrial. A naive geomean composite shows dogrider — the arena owner's own zerg io_uring WebSocket echo server — and fleck sitting on top. Objection sustained: they ran only 2 WebSocket profiles each. The naive composite is a geometric mean taken only over the profiles an entry actually ran — so a two-profile WebSocket specialist with monster echo numbers floats to the top on a sample size of two. The fix a fair ranking needs is coverage-weighting: average across all 27 profiles, zero-filling the ones an entry skipped. Do that and the two-trick acts sink, because every ducked profile becomes a zero dragging the mean down — which is exactly the point. A framework covering 24 profiles at 80% beats a specialist who only showed up twice. Even the platform owner's entry doesn't get to skip the work and keep the crown.
So the real findings of fact:
-
Most complete all-rounder:
aspnet-minimal, 24 profiles. -
Broadest performance leader:
genhttp-11, 14 head-to-head wins.
The verdict
On plaintext, pipelining, uploads, static files, CRUD, and WebSockets, the community challenger genhttp-11 outperforms Microsoft's flagship. On HTTP/2 — and on a database-bound async route — ASP.NET Core's mature stack wins so cleanly that on h2 GenHTTP shows up borrowing it; HTTP/3 splits down the middle. Both headline claims — "the lean engine is faster" and "Kestrel owns h2" — are, on this evidence, true.
This is one published round: a snapshot, not a statute. Re-check the leaderboard, audit the raw load-generator logs, and if your C# framework thinks it can take the stand — add it. The harness is open, the same Threadripper awaits, and ./scripts/validate.sh is the bailiff. Bring a dish to the cookout. Just, you know, prove it's good first.
Court is adjourned. See the full evidence at http-arena.com.




















