Skip to content

Latest commit

Β 

History

History
717 lines (576 loc) Β· 27.6 KB

File metadata and controls

717 lines (576 loc) Β· 27.6 KB

Hackney Architecture

This document describes the internal architecture of hackney 3.x, including the process-per-connection model, connection pooling, load regulation, and SSL handling.

Overview

Hackney uses a process-per-connection architecture where each HTTP connection runs in its own gen_statem process. This design provides:

  • Clean isolation - Each connection has its own state, no shared mutable state
  • Automatic cleanup - Process crashes clean up sockets automatically
  • Simple ownership - Socket always owned by connection process
  • OTP supervision - Standard supervisor tree for fault tolerance
hackney_sup
β”œβ”€β”€ hackney_manager          (connection registry)
β”œβ”€β”€ hackney_conn_sup         (connection supervisor)
β”‚   └── hackney_conn [1..N]  (connection processes for HTTP/1.1, HTTP/2)
β”œβ”€β”€ hackney_pools_sup        (pool supervisor)
β”‚   └── hackney_pool [1..N]  (pool processes)
└── hackney_altsvc           (Alt-Svc cache for HTTP/3 discovery)

QUIC connections (HTTP/3):
└── hackney_h3.erl           (HTTP/3 high-level + adapter over quic_h3)

Connection Process (hackney_conn)

Each connection is a gen_statem process that manages:

  • TCP/SSL socket
  • HTTP protocol state (request/response phases)
  • Streaming state (chunked encoding, content-length tracking)
  • Owner process monitoring

State Machine

                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚   created   β”‚
                    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ connect
                           β–Ό
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”Œβ”€β”€β”€β”€β”€β”‚  connected  │─────┐
              β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
              β”‚            β”‚            β”‚
        send_request       β”‚      upgrade_to_ssl
              β”‚            β”‚            β”‚
              β–Ό            β”‚            β–Ό
       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚  on_body   β”‚      β”‚     β”‚  connected β”‚ (SSL)
       β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜      β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚             β”‚
      finish_send_body     β”‚
             β”‚             β”‚
             β–Ό             β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
    β”‚ waiting_responseβ”‚    β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
             β”‚             β”‚
       start_response      β”‚
             β”‚             β”‚
             β–Ό             β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
      β”‚ on_status  β”‚       β”‚
      β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜       β”‚
            β”‚              β”‚
            β–Ό              β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
      β”‚ on_headers │────────
      β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜       β”‚
            β”‚              β”‚
            β–Ό              β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
    β”‚ on_resp_body β”‚       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜       β”‚
            β”‚              β”‚
            β”‚ body done    β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚  closing   β”‚
           β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β–Ό
              [exit]

Owner Monitoring

The connection process monitors its owner (the process that checked out the connection). If the owner crashes, the connection terminates automatically, preventing socket leaks.

%% When connection is checked out
MonitorRef = monitor(process, Owner),
%% If owner dies
{'DOWN', MonitorRef, process, Owner, _} -> terminate

Connection Pool (hackney_pool)

The pool stores TCP connections only for reuse. SSL connections are never pooled for security reasons (they close after use).

Why TCP-Only Pooling?

  1. Security - SSL session state should not be shared across requests
  2. Simplicity - No need to validate SSL session freshness
  3. Flexibility - TCP connections can be upgraded to SSL when needed

Pool State

-record(state, {
    name,                    %% Pool name
    max_connections,         %% Global max (legacy, per-host preferred)
    keepalive_timeout,       %% Max idle time (default 2000ms, max 2000ms)
    prewarm_count,           %% Connections to maintain per host (default 4)
    available = #{},         %% #{Key => [Pid]} - idle TCP connections
    in_use = #{},            %% #{Pid => Key} - checked out connections
    pid_monitors = #{},      %% #{Pid => MonitorRef}
    activated_hosts          %% Hosts with prewarm enabled
}).

Pool Operations

Checkout: Get an available TCP connection or none

hackney_pool:checkout(Host, Port, Transport, Opts)
%% Returns: {ok, PoolInfo, Pid} | {error, no_pool}

Checkin: Return a connection to the pool

hackney_pool:checkin(PoolInfo, Pid)
%% TCP connections are stored, SSL connections are closed

Keepalive Timeout

Idle connections are closed after keepalive_timeout (default and max: 2 seconds). This prevents:

  • Stale connections that the server has closed
  • Resource accumulation from unused connections
  • Issues with server-side connection limits

Load Regulation (hackney_load_regulation)

Per-host connection limits prevent overwhelming individual servers. This uses an ETS counting semaphore pattern for lock-free concurrent access.

How It Works

%% ETS table: hackney_host_limits
%% Key: {Host, Port} -> Value: current_count

%% Acquire a slot (blocks with backoff until available or timeout)
acquire(Host, Port, MaxPerHost, Timeout) ->
    Count = ets:update_counter(Table, Key, {2, 1}, {Key, 0}),
    case Count =< MaxPerHost of
        true -> ok;
        false ->
            ets:update_counter(Table, Key, {2, -1}),
            %% Backoff and retry until timeout
    end.

%% Release a slot
release(Host, Port) ->
    ets:update_counter(Table, Key, {2, -1, 0, 0}).

Why ETS Counting Semaphore?

  1. Lock-free - ets:update_counter is atomic
  2. Per-host isolation - Different hosts don't block each other
  3. No process bottleneck - No gen_server call for every request
  4. Backpressure - Requests wait when limit reached

Configuration

%% Default: 50 concurrent connections per host
hackney:get(URL, [], <<>>, [{max_per_host, 100}]).

%% Per-request timeout for acquiring a slot
hackney:get(URL, [], <<>>, [{checkout_timeout, 5000}]).

SSL Upgrade Strategy

HTTPS requests use TCP connection upgrade rather than direct SSL connections:

1. Get TCP connection (from pool or new)
2. Upgrade to SSL in-place: ssl:connect(Socket, SslOpts)
3. Use SSL connection for request
4. Close connection (SSL connections not pooled)
5. Trigger TCP prewarm for next HTTPS request

Benefits

  • Connection reuse - Pooled TCP connections can serve HTTP or HTTPS
  • Prewarm works for HTTPS - TCP connections ready to upgrade
  • Security - SSL state never shared between requests

Code Flow

%% In hackney.erl
connect_pool(Host, Port, Transport, Opts) ->
    %% Always checkout as TCP
    case hackney_pool:checkout(Host, Port, hackney_tcp, Opts) of
        {ok, PoolInfo, Pid} ->
            %% Upgrade if HTTPS
            case Transport of
                hackney_ssl ->
                    ok = hackney_conn:upgrade_to_ssl(Pid, SslOpts),
                    {ok, PoolInfo, Pid};
                _ ->
                    {ok, PoolInfo, Pid}
            end;
        ...
    end.

Protocol Selection

Hackney supports three HTTP protocols: HTTP/1.1, HTTP/2, and HTTP/3 (experimental). The protocol selection is controlled via the protocols option.

Default Protocols

By default, hackney uses [http2, http1]:

%% Default behavior - HTTP/2 preferred, HTTP/1.1 fallback
hackney:get("https://example.com/")

The default can be changed via application environment:

%% In sys.config or at runtime
application:set_env(hackney, default_protocols, [http2, http1]).

Enabling HTTP/3 (Experimental)

HTTP/3 uses QUIC (UDP transport). To enable HTTP/3:

%% Per-request: opt-in to HTTP/3
hackney:get("https://example.com/", [], <<>>, [
    {protocols, [http3, http2, http1]}
]).

%% Application-wide: enable HTTP/3 by default
application:set_env(hackney, default_protocols, [http3, http2, http1]).

Important considerations for HTTP/3:

  • Experimental - QUIC support is still maturing
  • UDP may be blocked - Corporate firewalls often block UDP

Protocol Priority

Protocols are tried in order. With [http3, http2, http1]:

  1. If QUIC is available and server supports HTTP/3: use HTTP/3
  2. Otherwise, ALPN negotiates HTTP/2 or HTTP/1.1 over TLS
  3. Server chooses the highest protocol it supports

Forcing a Single Protocol

%% Force HTTP/1.1 only (no HTTP/2 negotiation)
hackney:get(URL, [], <<>>, [{protocols, [http1]}]).

%% Force HTTP/2 only
hackney:get(URL, [], <<>>, [{protocols, [http2]}]).

%% HTTP/3 only (will fail if QUIC unavailable or server doesn't support it)
hackney:get(URL, [], <<>>, [{protocols, [http3]}]).

HTTP/3 and QUIC Architecture

HTTP/3 connections use QUIC (UDP-based transport) via a pure Erlang implementation from the quic dependency.

Event-Driven Architecture

Like TCP connections, QUIC uses an event-driven architecture where the owner process drives the connection:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Owner Process (Erlang)                       β”‚
β”‚                                                                  β”‚
β”‚  1. connect() β†’ Creates QUIC connection via quic library        β”‚
β”‚                                                                  β”‚
β”‚  2. Receives {select, Resource, Ref, ready_input}               β”‚
β”‚     └── Socket has data ready                                   β”‚
β”‚                                                                  β”‚
β”‚  3. Calls hackney_h3:process(ConnRef)                         β”‚
β”‚     └── Receives UDP packets                                    β”‚
β”‚     └── Processes QUIC frames                                   β”‚
β”‚     └── Triggers events (headers, data, etc.)                   β”‚
β”‚     └── Returns next timeout in ms                              β”‚
β”‚                                                                  β”‚
β”‚  4. Receives {h3, ConnRef, Event}                             β”‚
β”‚     └── {connected, Info}                                       β”‚
β”‚     └── {stream_headers, StreamId, Headers, Fin}                β”‚
β”‚     └── {stream_data, StreamId, Data, Fin}                      β”‚
β”‚     └── {closed, Reason}                                        β”‚
β”‚                                                                  β”‚
β”‚  5. Schedules timer: erlang:send_after(TimeoutMs, self(), ...)  β”‚
β”‚     └── Calls process() again when timer fires                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Pure Erlang Components

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       hackney_h3.erl                             β”‚
β”‚  - connect/4: Start QUIC connection via quic_h3                 β”‚
β”‚  - send_request/3: Open stream + send HEADERS                   β”‚
β”‚  - send_data/4: Send request body                               β”‚
β”‚  - reset_stream/3: Cancel an in-flight stream                   β”‚
β”‚  - close/2: Close connection                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   quic application (dependency)                  β”‚
β”‚  - QUIC protocol implementation (RFC 9000)                      β”‚
β”‚  - TLS 1.3 handshake                                            β”‚
β”‚  - Packet encoding/decoding                                     β”‚
β”‚  - Congestion control and loss recovery                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Connection Lifecycle

Owner Process                         quic library
     β”‚                                     β”‚
     β”‚  hackney_h3:connect(...)          β”‚
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ Create UDP socket
     β”‚                                     β”‚ Generate TLS keys
     β”‚                                     β”‚ Send Initial packet
     │◄───────────────────────────────────── {ok, ConnRef}
     β”‚                                     β”‚
     β”‚  {select, _, _, ready_input}        β”‚
     │◄───────────────────────────────────── UDP packet received
     β”‚                                     β”‚
     β”‚  hackney_h3:process(ConnRef)      β”‚
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ Process QUIC packets
     β”‚                                     β”‚ Continue handshake
     β”‚  {h3, ConnRef, {connected, Info}} β”‚
     │◄─────────────────────────────────────
     β”‚                                     β”‚
     β”‚  ... (request/response cycle) ...   β”‚
     β”‚                                     β”‚
     β”‚  hackney_h3:close(ConnRef, ...)   β”‚
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ Send CONNECTION_CLOSE
     β”‚  {h3, ConnRef, {closed, normal}}  β”‚
     │◄─────────────────────────────────────
     β”‚                                     β”‚

Benefits of Pure Erlang Implementation

Aspect Pure Erlang NIF-based
Portability All platforms Requires C compiler
Build complexity rebar3 compile CMake + dependencies
Debugging Erlang tools GDB/LLDB
Crash isolation Process crash Possible VM crash
Hot code loading Supported Limited

HTTP/2 Multiplexing

HTTP/2 connections are handled differently from HTTP/1.1. A single HTTP/2 connection can handle multiple concurrent requests via stream multiplexing.

HTTP/2 Pool Design

The pool maintains a separate map for HTTP/2 connections:

-record(state, {
    %% ... existing fields ...

    %% HTTP/2 connections: one per host, shared across callers
    h2_connections = #{}  %% #{Key => Pid}
}).

Key differences from HTTP/1.1 pooling:

Aspect HTTP/1.1 HTTP/2
Connections per host Multiple (pool) One (shared)
Checkout behavior Exclusive access Shared access
Checkin behavior Return to pool Keep in pool
Request handling Sequential Multiplexed streams

HTTP/2 Connection Flow

hackney:get("https://api.example.com/data")
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. Check for existing HTTP/2 conn   β”‚
β”‚    checkout_h2(Host, Port, ...)     β”‚
β”‚    β†’ Returns {ok, Pid} or none      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                   β”‚
   {ok, Pid}              none
   (reuse!)                 β”‚
        β”‚                   β–Ό
        β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚         β”‚ 2. Normal TCP flow  β”‚
        β”‚         β”‚    checkout β†’ new   β”‚
        β”‚         β”‚    β†’ upgrade SSL    β”‚
        β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                    β”‚
        β”‚                    β–Ό
        β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚         β”‚ 3. Check protocol   β”‚
        β”‚         β”‚    get_protocol()   β”‚
        β”‚         β”‚    β†’ http2 | http1  β”‚
        β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                    β”‚
        β”‚              β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
        β”‚              β”‚           β”‚
        β”‚           http2       http1
        β”‚              β”‚           β”‚
        β”‚              β–Ό           β”‚
        β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
        β”‚    β”‚ 4. Register H2  β”‚   β”‚
        β”‚    β”‚    register_h2()β”‚   β”‚
        β”‚    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
        β”‚             β”‚            β”‚
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜            β”‚
               β”‚                   β”‚
               β–Ό                   β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. Send request                     β”‚
β”‚    HTTP/2: assign StreamId          β”‚
β”‚    HTTP/1.1: send directly          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Stream Multiplexing in hackney_conn

Each hackney_conn process maintains a map of active HTTP/2 streams:

-record(conn_data, {
    %% HTTP/2 state machine (from cowlib)
    h2_machine :: tuple(),

    %% Active streams: #{StreamId => {Caller, State}}
    h2_streams = #{} :: #{
        pos_integer() => {gen_statem:from(), atom()}
    }
}).

When a request arrives:

%% In hackney_conn.erl
do_h2_request(From, Method, Path, Headers, Body, Data) ->
    %% 1. Get next stream ID from h2_machine
    {ok, StreamId, H2Machine1} = hackney_cow_http2_machine:init_stream(...),

    %% 2. Track caller for this stream
    Streams = maps:put(StreamId, {From, waiting_response}, Data#conn_data.h2_streams),

    %% 3. Send HEADERS frame (and DATA if body present)
    HeadersFrame = hackney_cow_http2:headers(StreamId, ...),
    Transport:send(Socket, HeadersFrame),

    %% 4. Return updated state (caller will receive reply when response arrives)
    {keep_state, Data#conn_data{h2_streams = Streams}}.

When a response arrives:

%% Response for StreamId received
handle_h2_frame({headers, StreamId, ...}, Data) ->
    %% Lookup caller from h2_streams
    {From, _State} = maps:get(StreamId, Data#conn_data.h2_streams),

    %% Reply to the correct caller
    gen_statem:reply(From, {ok, Status, Headers, Body}),

    %% Remove completed stream
    Streams = maps:remove(StreamId, Data#conn_data.h2_streams),
    {ok, Data#conn_data{h2_streams = Streams}}.

Benefits of HTTP/2 Multiplexing

  1. Reduced latency - No connection setup for subsequent requests
  2. Better resource usage - One TCP connection instead of many
  3. Head-of-line blocking avoided - Responses can arrive out of order
  4. Server efficiency - Servers prefer fewer connections with more streams

ALPN Protocol Negotiation

HTTP/2 is negotiated during TLS handshake via ALPN:

%% In hackney_ssl.erl
alpn_opts(Opts) ->
    Protocols = proplists:get_value(protocols, Opts, [http2, http1]),
    AlpnProtos = [proto_to_alpn(P) || P <- Protocols],
    [{alpn_advertised_protocols, AlpnProtos}].

proto_to_alpn(http2) -> <<"h2">>;
proto_to_alpn(http1) -> <<"http/1.1">>.

%% After connection
get_negotiated_protocol(SslSocket) ->
    case ssl:negotiated_protocol(SslSocket) of
        {ok, <<"h2">>} -> http2;
        _ -> http1
    end.

Connection Prewarm

After first use of a host, the pool maintains warm TCP connections ready for immediate use.

How It Works

  1. First request to api.example.com:443 completes
  2. On checkin, pool marks host as "activated"
  3. Pool creates prewarm_count (default 4) TCP connections
  4. Next request gets connection immediately (no connect latency)

Configuration

%% Global default
application:set_env(hackney, prewarm_count, 4).

%% Per-pool
hackney_pool:start_pool(mypool, [{prewarm_count, 8}]).

%% Explicit prewarm
hackney_pool:prewarm(default, "api.example.com", 443, 10).

Prewarm for HTTPS

When an SSL connection is checked in (and closed), the pool still triggers TCP prewarm. This ensures TCP connections are ready for the next HTTPS request to upgrade.

Request Flow

Complete flow for an HTTPS request with pooling:

hackney:get("https://api.example.com/data")
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. Load Regulation                  β”‚
β”‚    acquire("api.example.com", 443,  β”‚
β”‚            MaxPerHost, Timeout)     β”‚
β”‚    β†’ Blocks if at limit             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚ ok
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Pool Checkout                    β”‚
β”‚    checkout(Host, 443, hackney_tcp) β”‚
β”‚    β†’ Returns Pid or none            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                   β”‚
   {ok, Pid}              none
        β”‚                   β”‚
        β”‚                   β–Ό
        β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚         β”‚ Create new conn β”‚
        β”‚         β”‚ hackney_conn_supβ”‚
        β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                  β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. SSL Upgrade                      β”‚
β”‚    upgrade_to_ssl(Pid, SslOpts)     β”‚
β”‚    β†’ TCP socket becomes SSL         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. HTTP Request                     β”‚
β”‚    send_request(Pid, Method, ...)   β”‚
β”‚    recv_response(Pid)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. Checkin (async)                  β”‚
β”‚    Connection closed (SSL)          β”‚
β”‚    TCP prewarm triggered            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 6. Load Regulation Release          β”‚
β”‚    release("api.example.com", 443)  β”‚
β”‚    β†’ Slot available for next req    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Monitoring and Stats

Pool Stats

hackney_pool:get_stats(PoolName).
%% Returns:
%% [{name, PoolName},
%%  {max, MaxConnections},
%%  {in_use_count, InUse},
%%  {free_count, Free},
%%  {queue_count, 0}]  %% Always 0, load regulation handles queuing

Per-Host Stats

hackney_pool:host_stats(PoolName, Host, Port).
%% Returns:
%% [{active, N},    %% Currently in use (from load_regulation)
%%  {in_use, N},    %% Checked out from pool
%%  {free, N}]      %% Available in pool

Load Regulation Stats

hackney_load_regulation:current(Host, Port).
%% Returns: integer() - current concurrent connections to host

Advantages of This Architecture

vs. hackney 1.x

Aspect 1.x 2.x
State storage ETS tables Process state
Socket ownership Transferred between processes Always connection process
Error cleanup Manual via manager Automatic via process exit
SSL pooling Yes (security risk) No (TCP only)
Connection limits Global pool size Per-host limits
Prewarm No Yes

vs. Other HTTP Clients

Process isolation: Each connection is independent. A slow response on one connection doesn't block others. A crash in one connection doesn't affect others.

Backpressure: Load regulation naturally applies backpressure when a server is overwhelmed. Requests wait rather than creating unbounded connections.

Resource control: Per-host limits prevent a single slow host from consuming all connections. Different hosts are isolated.

SSL security: SSL connections are never reused, preventing session confusion attacks and ensuring fresh handshakes.

Prewarm efficiency: Frequently-used hosts have warm connections ready, eliminating connection latency for subsequent requests.

Configuration Reference

Pool Options

Option Default Description
pool_size / max_connections 50 Max connections in pool
timeout / keepalive_timeout 2000 Idle timeout (max 2000ms)
prewarm_count 4 Connections to maintain per host

Request Options

Option Default Description
pool default Pool name, or false for no pooling
max_per_host 50 Max concurrent connections to host
checkout_timeout 8000 Timeout to acquire connection slot
connect_timeout 8000 TCP connect timeout
recv_timeout 5000 Response receive timeout

Application Environment

%% In sys.config or application:set_env
{hackney, [
    {pool_handler, hackney_pool},
    {max_connections, 50},
    {timeout, 2000},
    {prewarm_count, 4}
]}.