Livery - high-performance HTTP/1.1, HTTP/2, HTTP/3 server for Erlang/OTP 27+

**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/req
  • gen_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.

1 Like