vanilla-epoll
Engine · V · engine: epoll
vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking epoll I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; json-tls terminates TLS 1.3 (Mbed TLS 4) on :8081 with ALPN http/1.1, reusing the same allocation-free /json serializer; static-tls serves the 20 /static files on :8081 through vanilla's static_assets (br/gzip negotiation; snapshots re-checked against the disk, so a replaced file is served at once), sending bodies of 16 KiB or more with sendfile(2) over kTLS; fortunes renders the DB rows + a runtime row with HTML escaping; async-db/crud/fortunes use the native async pg_async driver with per-worker pipelined pools. Pinned V master (built from source, bootstrap pinned). async parks each /delay/{ms} on a one-shot timerfd from a per-worker free-list and answers only once it reports an expiration; 8gbit echoes the POST body on the :8081 TLS listener (Content-Length or chunked, per-worker dechunk buffer); production-stack's /api cache-aside (X-Cache MISS/HIT, 1 s items / 30 s users TTL, invalidated on write) runs in-process — no Redis required. The h2c profiles run on an h2c-only epoll listener on :8082 (an HTTP/1.1 request there gets 505): the connection preface (PRI *) parses as an ordinary request, the handler hands the connection to vanilla's http2 module (core.queue_takeover) and appends the server SETTINGS, and every later burst goes to the RFC 9113 machine, which surfaces a request only when it is complete; /json reuses the same prefixes and body writer. unary-grpc takes the same preface on :8080 and answers GetSum as HEADERS, the SumReply as DATA, then grpc-status 0 as trailers. The echo-ws profiles share the :8080 engine: GET /ws validates the RFC 6455 upgrade, appends the 101 and hands the connection over (core.queue_takeover), after which every readable burst goes to a ConnHandler that walks the frames (echo, ping/pong, close) instead of the HTTP/1.1 parser; a pipelined burst is drained in one call and coalesced into one write, and the accept key is computed on the stack, so an upgrade allocates nothing. One worker per CPU of the container's cpuset.
Achievements
🥇 1 Gold · 🥈 1 Silver · 🥉 3 Bronze. Top three of a field, taken in this entry's own league: the family composite, the same composite among entries written in the same language, and each test profile it is scored on. Every field is the one the badges publish, so a medal and a badge always say the same thing. How medals are awarded.
| Medal | Award | Field | Rank |
|---|---|---|---|
| 🥇 Gold | Test profile | gRPC · Unary | 1 of 3 |
| 🥈 Silver | Test profile | HTTP/2 · Baseline (h2c) | 2 of 11 |
| 🥉 Bronze | Family composite | Composite · WebSocket | 3 of 18 |
| 🥉 Bronze | Test profile | Efficiency · Latency-1M | 3 of 37 |
| 🥉 Bronze | Test profile | WebSocket · Echo Pipelined | 3 of 18 |
Composite rank
Each profile of a family is worth 100 to the entry that leads it, and the composite is the sum over the family, less 2.5% for each of routing, middleware, request and response the entry does not do for you - its completeness factor, which the WebSocket and gRPC families do not carry. The field is this entry's own league: engines and reverse proxies are scored apart from frameworks. How it works.
| Family | Rank | Composite |
|---|---|---|
| HTTP/1.1 | 6 of 39 | 5531 |
| HTTP/2 | 5 of 14 | 1000 |
| Gateway | 1 of 2 | 2982 |
| gRPC | 3 of 3 | 1000 |
| WebSocket | 3 of 18 | 2362 |
Every result
Alpha Round, 35 runs. Requests per second is the best of three; latency, CPU and memory come from that run.
| Category | Profile | Conns | Req/sec | Avg | p99 | CPU | Memory |
|---|---|---|---|---|---|---|---|
| Connection | Baseline | 4,096 | 3,985,550 | 1.03ms | 1.22ms | 6461.3% | 138MiB |
| Connection | Pipelined | 4,096 | 38,972,931 | 1.68ms | 2.00ms | 6239.7% | 142MiB |
| Connection | Short-lived | 4,096 | 1,021,750 | 3.99ms | 40.90ms | 3135.8% | 63MiB |
| Concurrency | Async Delay | 32,000 | 2,602,119 | 12.19ms | 12.70ms | 6180.8% | 1.3GiB |
| Efficiency | Latency-10K | 1,024 | 9,981 | 70.5us | 97.0us | 21.4% | 68MiB |
| Efficiency | Latency-1M | 1,024 | 998,253 | 86.2us | 161.0us | 2159.5% | 72MiB |
| Efficiency | Latency-500K/8 | 1,024 | 498,857 | 952.7us | 9084.0us | 763.1% | 38MiB |
| Workload | JSON Comp | 4,096 | 2,506,534 | 1.41ms | 33.00ms | 5807.3% | 191MiB |
| Workload | JSON Comp | 16,384 | 2,450,176 | 6.30ms | 159.20ms | 5602.3% | 128MiB |
| Workload | JSON TLS | 4,096 | 1,561,516 | 14.01ms | 626.52ms | 5553.9% | 312MiB |
| Workload | 8Gbit | 512 | 49,171 | 283.5us | 235.0us | 505.2% | 97MiB |
| Workload | Static TLS | 1,024 | 1,127,202 | 1.40ms | 175.66ms | 6338.4% | 123MiB |
| Database | Async DB | 1,024 | 335,127 | 1.32ms | 8.95ms | 2975.5% | 136MiB |
| Database | Fortunes | 1,024 | 188,535 | 3.99ms | 7.80ms | 6286.9% | 245MiB |
| HTTP/2 | Baseline (h2c) | 256 | 15,623,717 | 852us | 21.85ms | 2115.5% | 167MiB |
| HTTP/2 | Baseline (h2c) | 1,024 | 15,521,245 | 4.08ms | 118.62ms | 2477.7% | 220MiB |
| HTTP/2 | Baseline (h2c) | 4,096 | 12,613,769 | 33.00ms | 544.30ms | 2778.3% | 1.9GiB |
| HTTP/2 | JSON (h2c) | 1,024 | 4,633,044 | 5.56ms | 115.05ms | 6432.8% | 474MiB |
| HTTP/2 | JSON (h2c) | 4,096 | 3,779,859 | 36.38ms | 295.71ms | 6410.1% | 1.6GiB |
| gRPC | Unary | 256 | 7,273,313 | 2.09ms | 23.10ms | 1381.9% | 136MiB |
| gRPC | Unary | 1,024 | 7,196,679 | 8.33ms | 100.64ms | 1527.5% | 400MiB |
| Gateway | Gateway (H2) | 512 | 380,490 | 49.37ms | 264.91ms | 5079.6% | 1.7GiB |
| Gateway | Gateway (H2) | 1,024 | 383,090 | 86.71ms | 564.50ms | 5258.6% | 2.3GiB |
| Gateway | Gateway (H3) | 64 | 92,115 | 22.11ms | 81.94ms | 2748.1% | 532MiB |
| Gateway | Gateway (H3) | 256 | 62,102 | 128.11ms | 759.12ms | 2123.9% | 1.5GiB |
| Gateway | Production Stack | 256 | 241,873 | 20.25ms | 115.42ms | 3838.9% | 1.2GiB |
| Gateway | Production Stack | 1,024 | 198,023 | 83.11ms | 475.44ms | 3420.9% | 2.1GiB |
| WebSocket | Echo | 512 | 4,340,961 | 117us | 148us | 6243.3% | 65MiB |
| WebSocket | Echo | 4,096 | 4,277,500 | 957us | 1.03ms | 6407.2% | 135MiB |
| WebSocket | Echo | 16,384 | 4,005,605 | 4.01ms | 6.98ms | 6370.6% | 369MiB |
| WebSocket | Echo Pipelined | 512 | 66,091,360 | 123us | 156us | 6462.1% | 64MiB |
| WebSocket | Echo Pipelined | 4,096 | 63,479,948 | 1.02ms | 1.21ms | 6147.8% | 122MiB |
| WebSocket | Echo Pipelined | 16,384 | 60,931,165 | 4.22ms | 4.47ms | 6019.8% | 368MiB |
| WebSocket | Echo Short-lived | 512 | 1,006,137 | 47us | 109us | 3424.9% | 62MiB |
| WebSocket | Echo Short-lived | 4,096 | 1,025,715 | 52us | 147us | 3087.7% | 94MiB |
Compared with
The V entries around vanilla-epoll on the HTTP/1.1 composite, and the one leading them. The last column shows every profile side by side.
| Entry | Composite | vs vanilla-epoll | |
|---|---|---|---|
| vanilla-io_uring | 6224 | +13% | head to head |
| viltrum | 405 | -93% | head to head |
Compare vanilla-epoll with any V entry · Compare vanilla-epoll with any entry, in any language · Every V entry, ranked
Results updated · How the benchmark is run · The machine · Add or fix an entry