Skip to content

Add blitz: Zig HTTP server with epoll multi-threading#21

Merged
MDA2AV merged 6 commits intoMDA2AV:mainfrom
BennyFranciscus:add-blitz
Mar 15, 2026
Merged

Add blitz: Zig HTTP server with epoll multi-threading#21
MDA2AV merged 6 commits intoMDA2AV:mainfrom
BennyFranciscus:add-blitz

Conversation

@BennyFranciscus
Copy link
Copy Markdown
Collaborator

blitz ⚡

The first Zig entry in HttpArena!

What is it?

A custom HTTP/1.1 server written in Zig, built from scratch for raw throughput. No framework dependencies — just Zig's standard library and Linux epoll.

Architecture

  • I/O model: epoll with edge-triggered notifications
  • Threading: SO_REUSEPORT — each CPU core gets its own listening socket and epoll loop, zero lock contention
  • HTTP parsing: Zero-copy request parser with pipeline batching (multiple requests parsed from a single read buffer)
  • Response strategy: JSON dataset and static files are pre-computed into full HTTP responses at startup
  • Memory: Per-connection buffers with minimal heap allocation in the hot path

Test profiles

  • baseline — GET/POST /baseline11 with query param parsing and body handling (including chunked)
  • pipelined — GET /pipeline returning fixed "ok" response
  • noisy — Same as baseline with error resilience
  • limited-conn — Same as baseline
  • json — Pre-computed JSON dataset response (~10 KB)
  • upload — Body ingestion, returns byte count

Why Zig?

Zig provides C-level performance with better safety guarantees, comptime optimizations, and cleaner ergonomics. No hidden allocations, no GC pauses, no runtime overhead. The language is ideal for this kind of systems-level performance work.

Source

Framework repo: https://github.com/BennyFranciscus/blitz

Built with Zig 0.14.0.

blitz is a custom HTTP/1.1 server written in Zig, designed for raw
throughput benchmarking. Key features:

- Pure Zig with no external dependencies (only libc for epoll)
- SO_REUSEPORT multi-threading: one epoll loop per CPU core
- Edge-triggered epoll with pipeline batching
- Zero-copy HTTP request parsing
- Pre-computed JSON and static file responses at startup
- Minimal heap allocations in the hot path

Language: Zig (first Zig entry in HttpArena)
Tests: baseline, pipelined, noisy, limited-conn, json, upload
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Hey @MDA2AV — the first CI run hit a port 8080 conflict (probably from the Kemal validation running at the same time). The second run is waiting on workflow approval since I'm a first-time contributor. Could you approve the workflow run when you get a chance? 🙏

Build works clean locally — Zig compiles in ~16s and the binary is tiny (~3MB). Happy to answer any questions about the implementation!

The noisy resilience validation test sends bad HTTP methods and expects
a 4xx response. Previously blitz would route any method to the handler,
returning 200 even for invalid methods like PATCH/DELETE/etc.

Now each endpoint validates the HTTP method:
- GET+POST: /baseline11, /baseline2, /upload
- GET only: /pipeline, /json, /static/*
- Returns 405 Method Not Allowed for anything else on known routes
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Found the validation failure — blitz was returning 200 for invalid HTTP methods (like PATCH/DELETE) on known routes. The noisy resilience test sends a bad method to /baseline11 and expects 4xx.

Fixed by adding method validation to all endpoints:

  • /baseline11, /baseline2, /upload: accept GET + POST only
  • /pipeline, /json, /static/*: accept GET only
  • Returns 405 Method Not Allowed for anything else on known routes

Zero-cost in the hot path — just a couple of byte comparisons before the actual handler runs. Should be a clean pass now 🤞

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: blitz | Profile: all profiles

blitz / baseline / 512c (p=1, r=0, cpu=unlimited)
  Best: 411208 req/s (CPU: 3613.6%, Mem: 115.3MiB) ===

blitz / baseline / 4096c (p=1, r=0, cpu=unlimited)
  Best: 432551 req/s (CPU: 3829.7%, Mem: 143.7MiB) ===

blitz / baseline / 16384c (p=1, r=0, cpu=unlimited)
  Best: 89797 req/s (CPU: 820.4%, Mem: 187.5MiB) ===

blitz / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 6390089 req/s (CPU: 3676.4%, Mem: 116.3MiB) ===

blitz / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 6660144 req/s (CPU: 4083.7%, Mem: 145.3MiB) ===

blitz / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 1413641 req/s (CPU: 845.3%, Mem: 253.0MiB) ===

blitz / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 432017 req/s (CPU: 3873.9%, Mem: 114.5MiB) ===

blitz / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 454293 req/s (CPU: 4206.2%, Mem: 144.6MiB) ===

blitz / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 403203 req/s (CPU: 3882.6%, Mem: 170.4MiB) ===

blitz / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 86481 req/s (CPU: 849.0%, Mem: 254.3MiB) ===

blitz / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 0 req/s (CPU: 0%, Mem: 0MiB) ===
Full log
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   4.14ms   4.91ms   6.89ms   10.70ms   18.30ms

  3195174 requests in 5.00s, 2004214 responses
  Throughput: 400.61K req/s
  Bandwidth:  3.16GB/s
  Status codes: 2xx=2004214, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2004211 / 2004214 responses (100.0%)
  Reconnects: 2005621
  Errors: connect 5, read 45, timeout 0
  CPU: 4029.9% | Mem: 168.1MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   4.20ms   4.95ms   7.03ms   10.90ms   18.20ms

  3158967 requests in 5.00s, 1980522 responses
  Throughput: 395.85K req/s
  Bandwidth:  3.13GB/s
  Status codes: 2xx=1980522, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1980519 / 1980522 responses (100.0%)
  Reconnects: 1981944
  Errors: connect 1, read 35, timeout 0
  CPU: 3925.6% | Mem: 172.3MiB

=== Best: 403203 req/s (CPU: 3882.6%, Mem: 170.4MiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / json / 16384c (p=1, r=0, cpu=unlimited) ===
==============================================
2cbee07d1e8bc5d3cd68a916f70499f33426f38b98defda87c74c3447c6e668a
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   87.22ms   99.60ms   126.40ms   467.40ms   527.40ms

  704374 requests in 5.02s, 413911 responses
  Throughput: 82.41K req/s
  Bandwidth:  666.28MB/s
  Status codes: 2xx=413911, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 413909 / 413911 responses (100.0%)
  Reconnects: 413186
  Errors: connect 0, read 20, timeout 0
  CPU: 888.1% | Mem: 265.3MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   75.79ms   99.60ms   124.40ms   143.70ms   165.60ms

  741772 requests in 5.03s, 435003 responses
  Throughput: 86.53K req/s
  Bandwidth:  699.65MB/s
  Status codes: 2xx=435003, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 435003 / 435003 responses (100.0%)
  Reconnects: 435357
  CPU: 849.0% | Mem: 254.3MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   76.98ms   101.60ms   125.80ms   145.00ms   168.60ms

  729432 requests in 5.03s, 428081 responses
  Throughput: 85.12K req/s
  Bandwidth:  688.24MB/s
  Status codes: 2xx=428081, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 428081 / 428081 responses (100.0%)
  Reconnects: 428425
  CPU: 869.6% | Mem: 264.0MiB

=== Best: 86481 req/s (CPU: 849.0%, Mem: 254.3MiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / upload / 64c (p=1, r=0, cpu=unlimited) ===
==============================================
9c67b55afec06347fac7bc086763ca2829d9672c584d92f746cc909ce6549a1c
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  0 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  CPU: 3.0% | Mem: 101.4MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  208 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  Reconnects: 208
  Errors: connect 0, read 208, timeout 0
  CPU: 0% | Mem: 117.4MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  3302 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  Reconnects: 3302
  Errors: connect 0, read 3302, timeout 0
  CPU: 46.5% | Mem: 131.8MiB

=== Best: 0 req/s (CPU: 0%, Mem: 0MiB) ===
httparena-bench-blitz
httparena-bench-blitz
[restore] Restoring CPU governor to powersave...

@MDA2AV
Copy link
Copy Markdown
Owner

MDA2AV commented Mar 15, 2026

@BennyFranciscus issues with upload

The upload test sends bodies larger than the 64KB fixed read buffer.
When the buffer filled up, parseRequest never found a complete request
and connections stalled (0 req/s).

Fix: promote to a heap-allocated overflow buffer when:
- Fixed buffer is full but no complete request parsed
- Already in overflow mode and more data arrives

Overflow caps at 4MB (MAX_REQUEST_SIZE). Falls back to fixed buffer
when data drains below 64KB. Zero overhead for normal requests that
fit in the fixed buffer.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Good catch — the upload was hitting 0 req/s because blitz uses a fixed 64KB read buffer, and the upload bodies are way bigger than that. The buffer would fill up, parseRequest never found a complete request, and everything stalled.

Fixed by adding a dynamic overflow buffer:

  • Normal requests (< 64KB) still use the zero-alloc fixed buffer — no overhead in the hot path
  • When the fixed buffer fills up without a complete request, it promotes to a heap-allocated buffer
  • Overflow grows as needed up to 4MB cap
  • Falls back to the fixed buffer once data drains below 64KB

Should pass the upload test now. 🤞

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: blitz | Profile: all profiles

blitz / baseline / 512c (p=1, r=0, cpu=unlimited)
  Best: 410996 req/s (CPU: 3658.2%, Mem: 117.2MiB) ===

blitz / baseline / 4096c (p=1, r=0, cpu=unlimited)
  Best: 425948 req/s (CPU: 3995.6%, Mem: 138.3MiB) ===

blitz / baseline / 16384c (p=1, r=0, cpu=unlimited)
  Best: 88710 req/s (CPU: 813.0%, Mem: 219.3MiB) ===

blitz / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 6372156 req/s (CPU: 3637.5%, Mem: 121.0MiB) ===

blitz / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 6695465 req/s (CPU: 3949.6%, Mem: 155.9MiB) ===

blitz / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 1441287 req/s (CPU: 863.1%, Mem: 216.8MiB) ===

blitz / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 447656 req/s (CPU: 3988.9%, Mem: 114.3MiB) ===

blitz / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 453649 req/s (CPU: 4348.0%, Mem: 140.6MiB) ===

blitz / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 405515 req/s (CPU: 3919.7%, Mem: 164.8MiB) ===

blitz / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 88029 req/s (CPU: 847.0%, Mem: 255.7MiB) ===

blitz / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 0 req/s (CPU: 0%, Mem: 0MiB) ===
Full log

  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   4.17ms   4.92ms   6.97ms   11.00ms   18.30ms

  3175955 requests in 5.00s, 1991736 responses
  Throughput: 398.13K req/s
  Bandwidth:  3.14GB/s
  Status codes: 2xx=1991736, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1991722 / 1991736 responses (100.0%)
  Reconnects: 1993115
  Errors: connect 2, read 42, timeout 0
  CPU: 4031.3% | Mem: 163.2MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   4.23ms   4.96ms   7.12ms   11.30ms   18.00ms

  3136957 requests in 5.00s, 1966462 responses
  Throughput: 393.05K req/s
  Bandwidth:  3.10GB/s
  Status codes: 2xx=1966462, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1966455 / 1966462 responses (100.0%)
  Reconnects: 1967985
  Errors: connect 5, read 30, timeout 0
  CPU: 3905.7% | Mem: 167.5MiB

=== Best: 405515 req/s (CPU: 3919.7%, Mem: 164.8MiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / json / 16384c (p=1, r=0, cpu=unlimited) ===
==============================================
f74261ab1f45ed1db971c8912b8356ae804acf49eda81f79ba5adfa6cb95df34
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   86.29ms   99.40ms   125.40ms   501.90ms   571.50ms

  705406 requests in 5.02s, 414601 responses
  Throughput: 82.62K req/s
  Bandwidth:  667.99MB/s
  Status codes: 2xx=414601, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 414601 / 414601 responses (100.0%)
  Reconnects: 413409
  Errors: connect 0, read 21, timeout 0
  CPU: 881.0% | Mem: 260.0MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   74.34ms   98.60ms   123.10ms   140.50ms   157.00ms

  752196 requests in 5.02s, 441909 responses
  Throughput: 87.97K req/s
  Bandwidth:  711.26MB/s
  Status codes: 2xx=441909, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 441908 / 441909 responses (100.0%)
  Reconnects: 442290
  CPU: 847.0% | Mem: 255.7MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/json
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   76.79ms   101.40ms   126.10ms   145.70ms   163.30ms

  731171 requests in 5.03s, 428713 responses
  Throughput: 85.31K req/s
  Bandwidth:  689.72MB/s
  Status codes: 2xx=428713, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 428713 / 428713 responses (100.0%)
  Reconnects: 429073
  CPU: 872.2% | Mem: 259.1MiB

=== Best: 88029 req/s (CPU: 847.0%, Mem: 255.7MiB) ===
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / upload / 64c (p=1, r=0, cpu=unlimited) ===
==============================================
16d64264985e37aba5bf5838f918b4587886bca9bfb36d60bb421347a2aff201
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  659172 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  Reconnects: 659176
  Errors: connect 0, read 659173, timeout 0
  CPU: 1734.3% | Mem: 818.6MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  661022 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  Reconnects: 661019
  Errors: connect 0, read 661014, timeout 0
  CPU: 1897.1% | Mem: 835.6MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency      0us      0us      0us      0us      0us

  668659 requests in 5.00s, 0 responses
  Throughput: 0 req/s
  Bandwidth:  0B/s
  Status codes: 2xx=0, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 0 / 0 responses (0.0%)
  Reconnects: 668663
  Errors: connect 0, read 668660, timeout 0
  CPU: 1708.9% | Mem: 850.9MiB

=== Best: 0 req/s (CPU: 0%, Mem: 0MiB) ===
httparena-bench-blitz
httparena-bench-blitz
[restore] Restoring CPU governor to powersave...

The read loop was treating EAGAIN/WouldBlock the same as real errors,
closing the connection immediately. For large uploads that arrive across
multiple epoll events, this killed the connection before the full body
arrived (hence 0 req/s on upload benchmark).

WouldBlock is the normal exit condition for edge-triggered epoll — it
means 'no more data right now, wait for next event'. Now we just break
out of the read loop and let the next epoll event deliver more data.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Found the upload bug! 🎯

The edge-triggered epoll read loop was treating WouldBlock (EAGAIN) the same as a real error — immediately closing the connection. For small requests this works because the entire request arrives in one read() call. But upload bodies are way bigger than 64KB, so they arrive across multiple epoll events:

  1. First event: read headers + partial body → buffer fills → read() returns WouldBlock
  2. Code treats WouldBlock as error → closes connection → 💀
  3. Client sees RST → retries → same thing → 0 req/s

Fix: WouldBlock is the normal exit for edge-triggered epoll — it means "no more data right now, wait for next event." Now we just break out of the read loop instead of setting should_close = true. The next epoll IN event delivers more data, and we keep accumulating in the overflow buffer until the full request is parsed.

The overflow buffer itself was fine — it just never got a chance to work because the connection was killed on the first partial read. 😅

Should be a clean upload pass now. 🤞

The upload test sends 20MB bodies (Content-Length: 20971520) but
MAX_REQUEST_SIZE was 4MB. The overflow buffer hit RequestTooLarge
and closed connections → 0 req/s with 660K+ read errors.

Bumped to 32MB to handle the benchmark's upload payload.
@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Still 0 req/s on upload after the WouldBlock fix — turns out it was a second bug. 🤦

The upload test sends 20MB bodies (Content-Length: 20971520) but MAX_REQUEST_SIZE was capped at 4MB. So the overflow buffer would grow until it hit the limit → RequestTooLarge error → connection closed → 660K read errors per run.

Bumped MAX_REQUEST_SIZE to 32MB. The WouldBlock fix was correct and necessary (we need to keep partial reads alive), but this cap was killing the connection before the body could fully arrive.

Should be a clean pass now — upload bodies will accumulate in the overflow buffer up to 32MB, which covers the ~20MB test payload with room to spare.

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: blitz | Profile: upload

blitz / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 315 req/s (CPU: 1195.9%, Mem: 4.1GiB) ===

blitz / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 287 req/s (CPU: 1710.4%, Mem: 8.0GiB) ===

blitz / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 282 req/s (CPU: 1892.8%, Mem: 11.0GiB) ===
Full log
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   199.49ms   141.70ms   326.90ms    1.11s    1.75s

  1572 requests in 5.00s, 1572 responses
  Throughput: 314 req/s
  Bandwidth:  26.69KB/s
  Status codes: 2xx=1572, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1572 / 1572 responses (100.0%)
  CPU: 1097.5% | Mem: 3.4GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     64 (1/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   198.24ms   150.40ms   235.30ms    1.18s    1.88s

  1578 requests in 5.00s, 1578 responses
  Throughput: 315 req/s
  Bandwidth:  26.79KB/s
  Status codes: 2xx=1578, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1578 / 1578 responses (100.0%)
  CPU: 1195.9% | Mem: 4.1GiB

=== Best: 315 req/s (CPU: 1195.9%, Mem: 4.1GiB) ===
  Input BW: 6.15GB/s (avg template: 20971593 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / upload / 256c (p=1, r=0, cpu=unlimited) ===
==============================================
ac814ce159562add9d5a01c4c13a4a0d01312ff515e022195c8a709919eb138c
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     256 (4/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   843.12ms   667.40ms    1.79s    3.45s    4.65s

  1241 requests in 5.00s, 1241 responses
  Throughput: 247 req/s
  Bandwidth:  21.07KB/s
  Status codes: 2xx=1241, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1241 / 1241 responses (100.0%)
  CPU: 1527.9% | Mem: 6.3GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     256 (4/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   725.89ms   450.00ms    1.65s    3.76s    4.68s

  1424 requests in 5.01s, 1424 responses
  Throughput: 284 req/s
  Bandwidth:  24.15KB/s
  Status codes: 2xx=1424, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1424 / 1424 responses (100.0%)
  CPU: 1381.8% | Mem: 7.3GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     256 (4/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   722.17ms   448.90ms    1.63s    3.42s    4.63s

  1438 requests in 5.00s, 1438 responses
  Throughput: 287 req/s
  Bandwidth:  24.41KB/s
  Status codes: 2xx=1438, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1438 / 1438 responses (100.0%)
  Latency overflow (>5s): 1
  CPU: 1710.4% | Mem: 8.0GiB

=== Best: 287 req/s (CPU: 1710.4%, Mem: 8.0GiB) ===
  Input BW: 5.61GB/s (avg template: 20971593 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / upload / 512c (p=1, r=0, cpu=unlimited) ===
==============================================
5318dc0466344357a6d8b49a7e8222a3b2113be6c9f1dabea4d3bbc59e109eb2
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    1.35s    1.13s    2.91s    4.50s    4.99s

  1221 requests in 5.01s, 1221 responses
  Throughput: 243 req/s
  Bandwidth:  20.72KB/s
  Status codes: 2xx=1221, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1221 / 1221 responses (100.0%)
  Latency overflow (>5s): 1
  CPU: 1637.6% | Mem: 8.6GiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    1.25s   911.70ms    2.87s    4.57s    4.93s

  1267 requests in 5.00s, 1267 responses
  Throughput: 253 req/s
  Bandwidth:  21.52KB/s
  Status codes: 2xx=1267, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1267 / 1267 responses (100.0%)
  Latency overflow (>5s): 1
  CPU: 1421.2% | Mem: 9.9GiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     512 (8/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    1.15s   785.20ms    2.84s    4.67s    4.94s

  1410 requests in 5.00s, 1410 responses
  Throughput: 281 req/s
  Bandwidth:  23.94KB/s
  Status codes: 2xx=1410, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1410 / 1410 responses (100.0%)
  CPU: 1892.8% | Mem: 11.0GiB

=== Best: 282 req/s (CPU: 1892.8%, Mem: 11.0GiB) ===
  Input BW: 5.51GB/s (avg template: 20971593 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz
[restore] Restoring CPU governor to powersave...

@BennyFranciscus
Copy link
Copy Markdown
Collaborator Author

Upload is working! 🎉

blitz / upload / 64c  → 315 req/s (CPU: 1195.9%, Mem: 4.1GiB)
blitz / upload / 256c → 287 req/s (CPU: 1710.4%, Mem: 8.0GiB)
blitz / upload / 512c → 282 req/s (CPU: 1892.8%, Mem: 11.0GiB)

Was a two-part bug:

  1. Edge-triggered epoll treated WouldBlock as an error — killed connections mid-body
  2. MAX_REQUEST_SIZE was 4MB, upload bodies are ~20MB

Both fixed. Memory usage is higher than ideal (4GB at 64c for upload) since each thread buffers the full body, but that's expected for a zero-framework approach without streaming. All profiles passing now ✅

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: blitz | Profile: all profiles

blitz / baseline / 512c (p=1, r=0, cpu=unlimited)
  Best: 2685775 req/s (CPU: 6713.8%, Mem: 102.4MiB) ===

blitz / baseline / 4096c (p=1, r=0, cpu=unlimited)
  Best: 3025250 req/s (CPU: 7027.5%, Mem: 138.8MiB) ===

blitz / baseline / 16384c (p=1, r=0, cpu=unlimited)
  Best: 2886406 req/s (CPU: 6676.1%, Mem: 305.2MiB) ===

blitz / pipelined / 512c (p=16, r=0, cpu=unlimited)
  Best: 38147193 req/s (CPU: 6806.7%, Mem: 100.4MiB) ===

blitz / pipelined / 4096c (p=16, r=0, cpu=unlimited)
  Best: 41648838 req/s (CPU: 6722.8%, Mem: 161.3MiB) ===

blitz / pipelined / 16384c (p=16, r=0, cpu=unlimited)
  Best: 38258073 req/s (CPU: 6479.5%, Mem: 346.2MiB) ===

blitz / limited-conn / 512c (p=1, r=10, cpu=unlimited)
  Best: 1623912 req/s (CPU: 4999.8%, Mem: 119.1MiB) ===

blitz / limited-conn / 4096c (p=1, r=10, cpu=unlimited)
  Best: 2092009 req/s (CPU: 6186.0%, Mem: 182.9MiB) ===

blitz / json / 4096c (p=1, r=0, cpu=unlimited)
  Best: 921105 req/s (CPU: 3048.4%, Mem: 167.9MiB) ===

blitz / json / 16384c (p=1, r=0, cpu=unlimited)
  Best: 1544488 req/s (CPU: 6136.4%, Mem: 484.3MiB) ===

blitz / upload / 64c (p=1, r=0, cpu=unlimited)
  Best: 308 req/s (CPU: 1113.3%, Mem: 3.4GiB) ===

blitz / upload / 256c (p=1, r=0, cpu=unlimited)
  Best: 280 req/s (CPU: 1457.5%, Mem: 7.2GiB) ===

blitz / upload / 512c (p=1, r=0, cpu=unlimited)
  Best: 255 req/s (CPU: 1460.6%, Mem: 9.8GiB) ===

blitz / noisy / 512c (p=1, r=0, cpu=unlimited)
  Best: 1680744 req/s (CPU: 5544.8%, Mem: 98.1MiB) ===

blitz / noisy / 4096c (p=1, r=0, cpu=unlimited)
  Best: 2466065 req/s (CPU: 6520.0%, Mem: 131.2MiB) ===

blitz / noisy / 16384c (p=1, r=0, cpu=unlimited)
  Best: 2140443 req/s (CPU: 6581.5%, Mem: 270.8MiB) ===
Full log

  WARNING: 2631558/10675543 responses (24.7%) had unexpected status (expected 2xx)
  CPU: 5433.0% | Mem: 99.6MiB

=== Best: 1680744 req/s (CPU: 5544.8%, Mem: 98.1MiB) ===
  Input BW: 169.91MB/s (avg template: 106 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / noisy / 4096c (p=1, r=0, cpu=unlimited) ===
==============================================
1a9213a56f54a43347cca6ca4b21e6d6d11d70c3ae106be23f7aa61da6752c6d
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    468us    252us    679us   3.22ms   17.90ms

  14042710 requests in 5.00s, 14042710 responses
  Throughput: 2.81M req/s
  Bandwidth:  210.02MB/s
  Status codes: 2xx=12330326, 3xx=0, 4xx=1712384, 5xx=0
  Latency samples: 14042710 / 14042710 responses (100.0%)
  Per-template: 6162295,6168810,1711605,0,0
  Per-template-ok: 6161897,6168429,0,0,0

  WARNING: 1712384/14042710 responses (12.2%) had unexpected status (expected 2xx)
  CPU: 6520.0% | Mem: 131.2MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    442us    251us    680us   2.95ms   15.90ms

  14184039 requests in 5.00s, 14183375 responses
  Throughput: 2.84M req/s
  Bandwidth:  211.49MB/s
  Status codes: 2xx=12288326, 3xx=0, 4xx=1895049, 5xx=0
  Latency samples: 14183355 / 14183375 responses (100.0%)
  Per-template: 6163832,6125292,1894231,0,0
  Per-template-ok: 6163425,6124887,0,0,0

  WARNING: 1895049/14183375 responses (13.4%) had unexpected status (expected 2xx)
  CPU: 6979.8% | Mem: 131.8MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    444us    246us    690us   3.23ms   10.30ms

  14639863 requests in 5.13s, 14639863 responses
  Throughput: 2.85M req/s
  Bandwidth:  212.53MB/s
  Status codes: 2xx=12621526, 3xx=0, 4xx=2018337, 5xx=0
  Latency samples: 14639822 / 14639863 responses (100.0%)
  Per-template: 6156303,6466026,2017493,0,0
  Per-template-ok: 6155880,6465618,0,0,0

  WARNING: 2018337/14639863 responses (13.8%) had unexpected status (expected 2xx)
  CPU: 6800.6% | Mem: 134.2MiB

=== Best: 2466065 req/s (CPU: 6520.0%, Mem: 131.2MiB) ===
  Input BW: 249.29MB/s (avg template: 106 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz

==============================================
=== blitz / noisy / 16384c (p=1, r=0, cpu=unlimited) ===
==============================================
4df58dba89dc0ddde5cbeb6bdaf349e265727ae23384a3a43c6ac57d05caa2aa
[wait] Waiting for server...
[ready] Server is up

[run 1/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   2.71ms   1.40ms   5.51ms   12.90ms   97.90ms

  13907461 requests in 5.00s, 13891077 responses
  Throughput: 2.78M req/s
  Bandwidth:  199.03MB/s
  Status codes: 2xx=10029900, 3xx=0, 4xx=3861177, 5xx=0
  Latency samples: 13891077 / 13891077 responses (100.0%)
  Per-template: 5019998,5012045,3859034,0,0
  Per-template-ok: 5018928,5010972,0,0,0

  WARNING: 3861177/13891077 responses (27.8%) had unexpected status (expected 2xx)
  CPU: 5974.5% | Mem: 259.6MiB

[run 2/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   2.55ms   1.41ms   5.84ms   12.90ms   49.70ms

  14668887 requests in 5.00s, 14652503 responses
  Throughput: 2.93M req/s
  Bandwidth:  210.01MB/s
  Status codes: 2xx=10586573, 3xx=0, 4xx=4065930, 5xx=0
  Latency samples: 14652503 / 14652503 responses (100.0%)
  Per-template: 5325788,5262842,4063873,0,0
  Per-template-ok: 5324785,5261788,0,0,0

  WARNING: 4065930/14652503 responses (27.7%) had unexpected status (expected 2xx)
  CPU: 6973.0% | Mem: 260.1MiB

[run 3/3]
gcannon — io_uring HTTP load generator
  Target:    localhost:8080/
  Threads:   64
  Conns:     16384 (256/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Templates: 5
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   2.53ms   1.41ms   5.87ms   11.80ms   44.30ms

  14758489 requests in 5.00s, 14742361 responses
  Throughput: 2.95M req/s
  Bandwidth:  211.48MB/s
  Status codes: 2xx=10702216, 3xx=0, 4xx=4040145, 5xx=0
  Latency samples: 14742361 / 14742361 responses (100.0%)
  Per-template: 5354660,5349623,4038078,0,0
  Per-template-ok: 5353619,5348597,0,0,0

  WARNING: 4040145/14742361 responses (27.4%) had unexpected status (expected 2xx)
  CPU: 6581.5% | Mem: 270.8MiB

=== Best: 2140443 req/s (CPU: 6581.5%, Mem: 270.8MiB) ===
  Input BW: 216.38MB/s (avg template: 106 bytes)
[dry-run] Results not saved (use --save to persist)
httparena-bench-blitz
httparena-bench-blitz
[skip] blitz does not subscribe to mixed
[skip] blitz does not subscribe to baseline-h2
[skip] blitz does not subscribe to static-h2
[skip] blitz does not subscribe to baseline-h3
[skip] blitz does not subscribe to static-h3
[skip] blitz does not subscribe to unary-grpc
[skip] blitz does not subscribe to unary-grpc-tls
[skip] blitz does not subscribe to echo-ws
[restore] Restoring CPU governor to powersave...

@MDA2AV MDA2AV merged commit 3213edf into MDA2AV:main Mar 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants