The HTTP Client That Deleted 1.3 Million Lines of C
Hackney v3.x replaced its C-backed HTTP/3 implementation with pure Erlang QUIC. That's not a refactor — it's a proof of concept for how the BEAM should work.
A few months ago, hackney — the Erlang HTTP client that most Elixir developers use without thinking about it — shipped v3.0. The headline on the hex.pm page reads “HTTP/3 support — Experimental QUIC/HTTP3 via pure Erlang.” The important word there isn’t “HTTP/3.” It’s “pure.”
The previous approach to HTTP/3 in the Erlang ecosystem involved wrapping MsQuic, Microsoft’s C implementation of the QUIC protocol. MsQuic is excellent software — it’s what powers Azure, among other things. But wrapping it meant shipping a NIF: a native extension that calls into C from the BEAM. That’s roughly 1.3 million lines of C in your dependency tree, along with all the build complexity that implies (cmake, clang, platform-specific compilation steps, Docker layer considerations) and a risk profile that’s fundamentally different from pure Erlang code.
The reason that risk profile matters is specific to the BEAM’s execution model. When an Erlang process crashes, the runtime knows about it. The supervisor tree knows about it. The crash is isolated, it’s recoverable, and it produces a proper exit signal that other processes can observe and respond to. When a NIF crashes — which C code can do for all kinds of reasons — the situation is different. A badly-behaved NIF can take down the entire VM. The BEAM’s fault tolerance guarantees depend on a contract that C code can break.
Hackney’s new HTTP/3 stack is a gen_server. Each QUIC connection is managed as an OTP process. The supervisor tree is straightforward: there’s a hackney_conn_sup running simple_one_for_one to dynamically spawn connection workers; the pool management is a separate gen_server that handles checkout/checkin semantics for HTTP/1, HTTP/2, and HTTP/3 connections uniformly. When a connection dies, it’s a process exit. The pool handles it. No VM crash, no opaque C pointer gone dangling.
%% From hackney_quic.erl: the gen_server init, connecting via pure Erlang quic library
init({Host, Port, Opts, Owner}) ->
MonRef = erlang:monitor(process, Owner),
QuicOpts = build_quic_opts(Host, Opts),
case quic:connect(binary_to_list(Host), Port, QuicOpts, self()) of
{ok, QuicConn} ->
register_conn(QuicConn, self()),
{ok, #state{quic_conn = QuicConn, owner = Owner, owner_mon = MonRef,
host = Host, port = Port}};
{error, Reason} ->
{stop, Reason}
end.
The quic library itself (from EMQX) is still a NIF wrapping MsQuic at the lowest level — that’s unavoidable for actual QUIC transport. But the HTTP/3 framing, QPACK header compression, stream management, and connection lifecycle are all implemented in pure Erlang, sitting on top of quic as a narrow transport primitive. The OTP supervision model gets to own all of the interesting state. That’s the meaningful line.
What makes this worth writing about isn’t that HTTP/3 exists in the Elixir ecosystem — it’s that the decision to implement it in pure Erlang when C was already available reveals something specific about where the BEAM community’s architectural values have landed. The precedent was easy: MsQuic was there, the NIF pattern was established, and it worked. The choice to write a full H3 framing layer, QPACK encoder, and connection manager in Erlang anyway — and to do it well enough that is_available/0 just unconditionally returns true — is a statement about what kind of library hackney wants to be.
HTTP/3 is still opt-in: {protocols, [http3, http2, http1]} in your hackney config. But that it’s there, and that it’s supervised, and that you don’t need cmake to use it — that’s the story.
Enjoy this issue?
Get ElixirLens every Monday — sharp takes on Elixir and the trends shaping it.