# hackney for Erlang: HTTP/1.1, HTTP/2, streaming, and experimental HTTP/3

> hackney is an Erlang HTTP client for synchronous requests, streamed payloads, asynchronous responses, WebSockets, and negotiated HTTP/2. It also offers experimental HTTP/3, but its custom licence and native QUIC dependencies deserve review before adoption.

**benoitc/hackney** — simple HTTP client in Erlang

- Repository: https://github.com/benoitc/hackney
- Stars: 1,419 · Forks: 459
- Language: Erlang
- License: NOASSERTION
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/benoitc-hackney

## Where hackney fits in an Erlang service

hackney addresses the need for an HTTP client library inside Erlang or Elixir applications. It is aimed at services that send ordinary requests, receive large bodies, maintain long-lived connections, or consume HTTP protocols beyond a basic HTTP/1.1 exchange. Convenience functions cover GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH and QUERY. Lower-level interfaces are available when the application needs a streaming request body, an asynchronous response, a WebSocket, HTTP/2 streams, or WebTransport.

The library is broad for a client library, but that breadth creates distinct operating profiles. A conventional JSON API can use `hackney:get/1` and `hackney:post/3`. File transfer code can stream chunks rather than assemble one large term. A real-time service can keep a WebSocket connection. HTTP/2 is selected automatically when the server supports it, while HTTP/3 is deliberately opt-in and described as experimental.

This is not a server framework, command-line utility, browser automation package or testing proxy. Its scope is outbound client behavior. It is relevant when the application itself needs to manage sockets, protocol negotiation and connection lifecycle, rather than delegating those concerns to another layer.

## Connections, pools, TLS, and protocol negotiation

Each connection runs in its own `gen_statem` process. The README says this design isolates connections and allows automatic cleanup after crashes. Requests are pooled by default, either through the default pool or a named pool. A named pool can carry a `max_connections` setting, and a one-off request can disable pooling with `{pool, false}`.

TLS is enabled by default with certificate verification using Mozilla's CA bundle. The default HTTPS behavior has an important wrinkle: only plain TCP connections remain pooled by default, and the connection is upgraded to TLS for the request, then closed. Reusing HTTPS/1.1 requires `{ssl_pooling, true}`. Pooled TLS connections are keyed by TLS options, so callers using different `ssl_options` do not share them accidentally. Default TLS configuration enables TLS 1.3 session resumption, while custom `ssl_options` do not because the shared OTP ticket store can resume a session without repeating certificate validation.

HTTP/2 is not selected through a separate client package in the documented interface. The client uses ALPN negotiation and can fall back according to the requested protocol list. `{protocols, [http1]}` forces HTTP/1.1, while `{protocols, [http2]}` forces HTTP/2. The broader list beginning with `http3` opts into the experimental QUIC path.

That model is useful, but the default SSL pooling policy may surprise developers who see "connection pooling" as applying to every HTTPS request. It also means deployment decisions about TLS options directly affect reuse and session behavior.

## Install hackney and send the first GET request

For Rebar3 projects, the README gives `hackney` as a dependency. Add it under the application's `rebar.config` file before compiling the project:

```erlang
{deps, [hackney]}.
```

An Elixir project managed by Mix uses a version constraint instead:

```elixir
{:hackney, "~> 4.0"}
```

Once the application dependency is available, start hackney and issue a GET request. The following example follows the Quick Start section and expects the returned tuple to contain status `200`:

```erlang
application:ensure_all_started(hackney).

{ok, 200, _Headers, Body} =
    hackney:get(<<"https://httpbin.org/get">>).
```

The response body is returned in the tuple for a synchronous request. A POST request can add headers and a binary payload through `hackney:post/3`:

```erlang
Headers = [{<<"content-type">>, <<"application/json">>}],
Payload = <<"{\"key\": \"value\"}">>,
{ok, 200, _, _} =
    hackney:post(<<"https://httpbin.org/post">>, Headers, Payload).
```

This tutorial depends on `rebar3` being available because the repository Makefile delegates compilation and tests to it. The Makefile also defines `test`, `dialyzer`, `doc`, HTTP/2 end-to-end checks and HTTP/3 end-to-end targets, although those are developer workflows rather than required steps for a normal dependency installation.

## Streaming and asynchronous response handling

The convenience API returns a complete response body, which is simple but unsuitable when the application should avoid retaining a large payload in memory. hackney therefore separates streaming request bodies from streamed response delivery. For uploads, `hackney:post/3` starts with the atom `stream`; `hackney:send_body/2` sends chunks, and `hackney:finish_send_body/1` completes the body. The application then calls `hackney:start_response/1` before reading the response.

Response streaming uses the asynchronous API rather than the same request-reference flow. With `[async]`, hackney sends status and headers to the calling Erlang process as `hackney_response` messages. Further messages carry body data, followed by `{hackney_response, Ref, done}`. The consumer matches on the request reference so unrelated response messages do not get mistaken for the current request.

This design lets an Erlang process perform other work while waiting for response data. It also places message handling on the application side. The receiving loop must handle ordering, completion and any unexpected messages for the request. The README does not document cancellation or rollback for an in-flight asynchronous response, so those cases should be checked in the API reference before assuming the operation can be abandoned safely.

For a large upload, stream sends. For a large download, asynchronous messages. A request can use both, but each API requires explicit completion calls and careful reference handling.

## HTTP/2, WebSocket, WebTransport, and the QUIC boundary

HTTP/2 brings multiplexing, header compression and flow control according to the README. Dedicated `h2_*` functions support bidirectional streams with interleaved sending and receiving, trailers and flow control. This is the relevant interface for workloads that need more than a single ordinary response, although the README excerpt does not show a complete `h2_*` call sequence.

WebSocket support uses a connection value returned by `hackney:ws_connect/1`. The client then sends typed frames with `hackney:ws_send/2`, receives them through `hackney:ws_recv/1`, and closes through `hackney:ws_close/1`. WebTransport deliberately resembles that interface: `hackney:wt_connect/1`, `hackney:wt_send/2`, `hackney:wt_recv/1` and `hackney:wt_close/1`. According to the documentation, WebTransport can run over HTTP/3 or HTTP/2.

HTTP/3 has a different adoption boundary from the other protocols. The README calls it experimental, describes the implementation as pure Erlang, and requires an explicit protocol list. The repository Makefile adds another complication: its `quic-deps` target vendors `ngtcp2` at `v1.18.0` and `nghttp3` at `v1.9.0`, and the HTTP/3 test output references `priv/hackney_quic.so` and `priv/hackney_quic.dll`. These details show a native build and vendoring path that goes beyond ordinary application dependency management.

HTTP/2 can be treated as part of normal protocol negotiation if the deployment environment supports it. HTTP/3 should remain a separately reviewed path until its native dependencies, target platforms and operational behavior match the application's requirements.

## Deployment, maintenance, and licence constraints

The repository is not archived, and the last push was on 2026-10-03. Releases 4.8.5, 4.8.4 and 4.8.3 were published on 2026-10-03, 2026-09-27 and 2026-09-27 respectively. This is evidence of recent code and release activity, but the README does not define a long-term support window or a compatibility policy for older releases. Users of version 1.x are directed to a separate migration guide, which means API compatibility across major generations should not be assumed.

Upgrade cost depends on which features are used. An ordinary HTTP client adoption involves dependency management, request behavior and pool configuration. Moving from asynchronous APIs to HTTP/2 or HTTP/3 adds protocol selection, server compatibility, native build dependencies and new message or stream semantics. The repository includes `CONTRIBUTING.md`, `DEVELOPMENT.md`, `NEWS.md`, `SECURITY.md` and a migration guide, but the README does not summarize the effort needed for each upgrade.

 That makes the licence text a gating document for adoption. The article cannot state permissions beyond what the file says because its contents are not available. An organization should verify redistribution, modification, notice and dependency obligations directly in `LICENSE`, `NOTICE` and the relevant native QUIC dependency terms.

For operations, the main cost is not compilation alone. Pool sizing, TLS reuse, protocol fallback, native libraries and server-side support all affect deployment. A team that only needs basic HTTPS calls may carry unnecessary complexity if it enables HTTP/3 without a corresponding server path.

## Choosing hackney against a simpler client boundary

The real alternative is an Erlang HTTP stack centered on synchronous calls with a smaller protocol surface. Such a client may expose `request/1`-style calls, connection management and SSL options without WebSocket, bidirectional HTTP/2 stream, WebTransport or HTTP/3 interfaces. The precise competitor cannot be named from the available project documentation without inventing details, but the architectural difference is clear: hackney combines a general HTTP client with persistent protocol-specific connection models.

That distinction matters. A service making occasional requests to JSON endpoints can prefer the smallest client surface that meets its requirements. hackney becomes more relevant when connection reuse, streamed bodies, message-based responses or persistent WebSockets are central to the design. It is also relevant when automatic HTTP/2 negotiation should not require a separate client-facing abstraction in the application.

hackney is a poor fit when the application wants a deliberately synchronous, small API and has no use for streaming or persistent connection protocols. It is also a poor fit where the custom licence is incompatible with organizational policy. HTTP/3 remains a specific reason for caution rather than an automatic benefit, because the README labels it experimental and the Makefile reveals native QUIC setup targets.

The key adoption test is not feature count. It is whether the application benefits from one client that spans HTTP/1.1, HTTP/2, HTTP/3, WebSocket and WebTransport while accepting the corresponding configuration, build and testing costs.

## Conclusion

Erlang teams that need a client-centered HTTP stack with pooled connections, streaming and protocol negotiation should evaluate hackney. Teams that require an OSI-approved licence or want HTTP/3 treated as a settled feature should verify LICENSE and the QUIC setup before committing. For a first adoption, confirm connection reuse for the intended HTTPS topology, exercise both synchronous and asynchronous response handling, then decide whether experimental HTTP/3 belongs in the production path.

## FAQ

### How do I install hackney in an Erlang or Elixir project?

For Rebar3, the README specifies `{deps, [hackney]}.` in the dependency configuration. For Mix, it specifies `{:hackney, "~> 4.0"}`.

### How do I send a streamed HTTP request body with hackney?

Pass `stream` as the request body, send each chunk with `hackney:send_body/2`, and call `hackney:finish_send_body/1`. After that, call `hackney:start_response/1` to obtain the response status and headers.

### Is HTTP/3 enabled by default in hackney?

No. The README describes HTTP/3 as experimental and opt-in. A request must include a protocol list containing `http3`, while the Makefile also provides targets for vendoring and cleaning the QUIC dependencies.

## Sources

- [benoitc/hackney on GitHub](https://github.com/benoitc/hackney)
- [Issues](https://github.com/benoitc/hackney/issues)
- [README](https://github.com/benoitc/hackney/blob/master/README.md)
- [Releases](https://github.com/benoitc/hackney/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/benoitc-hackney
