**Profiling led to a 2.4× win on large HTTP/2 responses **
I’ve been benchmarking Livery against **cowboy ** and **bandit **, and HTTP/2 large-body throughput was lagging roughly 2× behind bandit at 100 KiB.
I started to play with the “flow control” , bumping the client’s window (h2load -w24 -W24), but it changed **nothing **. So it was not flow control.
eprof on the out-of-process server (driven by external h2load) told me the real story, per 100 KiB response:
ssl:send/2: **8.0/req ** (one TLS write per DATA frame)crypto:aead_cipher_nif: ~15/reqgen_statem:call: 12.6/req
ssl:send afaik *is * a gen_statem:call to the TLS connection process. So writing one frame at a time meant ~8 round-trips + 8 records + ~15 encryptions per response. The h2 library has the be fixed to coalesce a response’s frames into a **single ** ssl:send (one iolist) while still respecting flow control and max_frame_size, only the write are batched.
Before → after (Livery, HTTP/2 over TLS)
| Body | before (h2 0.8.0) | after (h2 0.9.0) |
|---|---|---|
tiny GET |
~128k | ~137k |
| 1 KiB | ~122k | ~133k |
| 10 KiB | ~114k | ~123k |
| 100 KiB | **~28k ** | **~66k ** |
echo POST |
~108k | ~118k |
Cross-server, HTTP/2 over TLS (req/s, after the fix)
| Body | livery | cowboy | bandit |
|---|---|---|---|
tiny GET |
**137k ** | 80k | 125k |
| 1 KiB | **133k ** | 80k | 118k |
| 10 KiB | **123k ** | 80k | 107k |
| 100 KiB | **66k ** | 80k | 62k |
echo POST |
**118k ** | 40k | 104k |
With latest change the large-body gap is closed: at 100 KiB Livery went from 28k to 66k (2.4×), now ahead of bandit and approaching cowboy; smaller bodies gained ~7-9% from the same change. (cowboy’s flat ~80k is its own HTTP/2 behaviour under h2load’s stream churn.) . The remaining gap with Cowboy is mostly due to our current design and will be hard to eliminate without compromising resilience: every request is counted so we can protect against DDoS.
The benchmark stools are available on git.