Context
We're using gun_pool with HTTP/1.1 pools of 300+ connections for high-throughput services. We've noticed that checkout latency increases linearly with pool size, and traced it to find_available_connection/1 in gun_pool.erl.
Current behavior
On every checkout, the function:
- Extracts all connection PIDs from the map —
maps:keys(Conns) — O(n)
- Pairs each with a random value and sorts the list — O(n log n)
- Linearly scans for a connection with available capacity — O(n) worst case
find_available_connection(#state{table=Tid, conns=Conns}) ->
I = lists:sort([{rand:uniform(), K} || K <- maps:keys(Conns)]),
find_available_connection(I, Conns, Tid).
For HTTP/1.1 (max_streams = 1), under load most connections are busy, so the scan often walks through a large portion of the list before finding a free one. Since checkout is a synchronous gen_server:call, all checkouts are serialized through the pool process, and the O(n log n) work per call becomes a bottleneck.
For HTTP/2 with multiplexing this is less of a problem because pools tend to be smaller and connections almost always have capacity, but it still means requests are not balanced across connections.
Question
Is the random shuffle approach intentional for a specific reason (e.g. fairness, simplicity), or would you be open to an alternative?
Possible approach
One option could be a priority queue (or similar structure) keyed by current stream count, which would give O(log n) checkout and naturally balance load across connections for multiplexed protocols. The queue would be updated on stream open/close via the existing event handler.
This would also make checkout cost independent of pool size, which matters when pools are large.
Happy to discuss further or contribute a PR if you're interested.
Context
We're using
gun_poolwith HTTP/1.1 pools of 300+ connections for high-throughput services. We've noticed that checkout latency increases linearly with pool size, and traced it tofind_available_connection/1ingun_pool.erl.Current behavior
On every checkout, the function:
maps:keys(Conns)— O(n)For HTTP/1.1 (
max_streams = 1), under load most connections are busy, so the scan often walks through a large portion of the list before finding a free one. Since checkout is a synchronousgen_server:call, all checkouts are serialized through the pool process, and the O(n log n) work per call becomes a bottleneck.For HTTP/2 with multiplexing this is less of a problem because pools tend to be smaller and connections almost always have capacity, but it still means requests are not balanced across connections.
Question
Is the random shuffle approach intentional for a specific reason (e.g. fairness, simplicity), or would you be open to an alternative?
Possible approach
One option could be a priority queue (or similar structure) keyed by current stream count, which would give O(log n) checkout and naturally balance load across connections for multiplexed protocols. The queue would be updated on stream open/close via the existing event handler.
This would also make checkout cost independent of pool size, which matters when pools are large.
Happy to discuss further or contribute a PR if you're interested.