Skip to content

Connection Pools

While the top-level API provides convenience functions for working with httpcore, in practice you'll almost always want to take advantage of the connection pooling functionality that it provides.

To do so, instantiate a pool instance, and use it to send requests:

import httpcore

http = httpcore.ConnectionPool()
r = http.request("GET", "https://www.example.com/")

print(r)
# <Response [200]>

Connection pools support the same .request() and .stream() APIs as described in the Quickstart.

We can observe the benefits of connection pooling with a simple script like so:

import httpcore
import time


http = httpcore.ConnectionPool()
for counter in range(5):
    started = time.time()
    response = http.request("GET", "https://www.example.com/")
    complete = time.time()
    print(response, "in %.3f seconds" % (complete - started))

The output should demonstrate the initial request as being substantially slower than the subsequent requests:

<Response [200]> in {0.529} seconds
<Response [200]> in {0.096} seconds
<Response [200]> in {0.097} seconds
<Response [200]> in {0.095} seconds
<Response [200]> in {0.098} seconds

This is to be expected. Once we've established a connection to "www.example.com" we're able to reuse it for following requests.

Configuration

The connection pool instance is also the main point of configuration. Let's take a look at the various options that it provides:

SSL configuration

  • ssl_context: An SSL context to use for verifying connections. If not specified, the default httpcore.default_ssl_context() will be used.

Pooling configuration

  • max_connections: The maximum number of concurrent HTTP connections that the pool should allow. Any attempt to send a request on a pool that would exceed this amount will block until a connection is available.
  • max_keepalive_connections: The maximum number of idle HTTP connections that will be maintained in the pool.
  • keepalive_expiry: The duration in seconds that an idle HTTP connection may be maintained for before being expired from the pool.

HTTP version support

  • http1: A boolean indicating if HTTP/1.1 requests should be supported by the connection pool. Defaults to True.
  • http2: A boolean indicating if HTTP/2 requests should be supported by the connection pool. Defaults to False.

Other options

  • retries: The maximum number of retries when trying to establish a connection.
  • local_address: Local address to connect from. Can also be used to connect using a particular address family. Using local_address="0.0.0.0" will connect using an AF_INET address (IPv4), while using local_address="::" will connect using an AF_INET6 address (IPv6).
  • uds: Path to a Unix Domain Socket to use instead of TCP sockets.
  • network_backend: A backend instance to use for handling network I/O.
  • socket_options: Socket options that have to be included in the TCP socket when the connection was established.

Pool lifespans

Because connection pools hold onto network resources, careful developers may want to ensure that instances are properly closed once they are no longer required.

Working with a single global instance isn't a bad idea for many use case, since the connection pool will automatically be closed when the __del__ method is called on it:

# This is perfectly fine for most purposes.
# The connection pool will automatically be closed when it is garbage collected,
# or when the Python interpreter exits.
http = httpcore.ConnectionPool()

However, to be more explicit around the resource usage, we can use the connection pool within a context manager:

with httpcore.ConnectionPool() as http:
    ...

Or else close the pool explicitly:

http = httpcore.ConnectionPool()
try:
    ...
finally:
    http.close()

Thread and task safety

Connection pools are designed to be thread-safe. Similarly, when using httpcore in an async context connection pools are task-safe.

This means that you can have a single connection pool instance shared by multiple threads.


Reference

httpcore.ConnectionPool

A connection pool for making HTTP requests.

connections: List[httpcore.ConnectionInterface] property readonly

Return a list of the connections currently in the pool.

For example:

>>> pool.connections
[
    <HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 6]>,
    <HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 9]> ,
    <HTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>,
]

__init__(self, ssl_context=None, max_connections=10, max_keepalive_connections=None, keepalive_expiry=None, http1=True, http2=False, retries=0, local_address=None, uds=None, network_backend=None, socket_options=None) special

A connection pool for making HTTP requests.

Parameters:

Name Type Description Default
ssl_context Optional[ssl.SSLContext]

An SSL context to use for verifying connections. If not specified, the default httpcore.default_ssl_context() will be used.

None
max_connections Optional[int]

The maximum number of concurrent HTTP connections that the pool should allow. Any attempt to send a request on a pool that would exceed this amount will block until a connection is available.

10
max_keepalive_connections Optional[int]

The maximum number of idle HTTP connections that will be maintained in the pool.

None
keepalive_expiry Optional[float]

The duration in seconds that an idle HTTP connection may be maintained for before being expired from the pool.

None
http1 bool

A boolean indicating if HTTP/1.1 requests should be supported by the connection pool. Defaults to True.

True
http2 bool

A boolean indicating if HTTP/2 requests should be supported by the connection pool. Defaults to False.

False
retries int

The maximum number of retries when trying to establish a connection.

0
local_address Optional[str]

Local address to connect from. Can also be used to connect using a particular address family. Using local_address="0.0.0.0" will connect using an AF_INET address (IPv4), while using local_address="::" will connect using an AF_INET6 address (IPv6).

None
uds Optional[str]

Path to a Unix Domain Socket to use instead of TCP sockets.

None
network_backend Optional[httpcore.NetworkBackend]

A backend instance to use for handling network I/O.

None
socket_options Optional[Iterable[Union[Tuple[int, int, int], Tuple[int, int, Union[bytes, bytearray]], Tuple[int, int, NoneType, int]]]]

Socket options that have to be included in the TCP socket when the connection was established.

None
Source code in httpcore/__init__.py
def __init__(
    self,
    ssl_context: Optional[ssl.SSLContext] = None,
    max_connections: Optional[int] = 10,
    max_keepalive_connections: Optional[int] = None,
    keepalive_expiry: Optional[float] = None,
    http1: bool = True,
    http2: bool = False,
    retries: int = 0,
    local_address: Optional[str] = None,
    uds: Optional[str] = None,
    network_backend: Optional[NetworkBackend] = None,
    socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
) -> None:
    """
    A connection pool for making HTTP requests.

    Parameters:
        ssl_context: An SSL context to use for verifying connections.
            If not specified, the default `httpcore.default_ssl_context()`
            will be used.
        max_connections: The maximum number of concurrent HTTP connections that
            the pool should allow. Any attempt to send a request on a pool that
            would exceed this amount will block until a connection is available.
        max_keepalive_connections: The maximum number of idle HTTP connections
            that will be maintained in the pool.
        keepalive_expiry: The duration in seconds that an idle HTTP connection
            may be maintained for before being expired from the pool.
        http1: A boolean indicating if HTTP/1.1 requests should be supported
            by the connection pool. Defaults to True.
        http2: A boolean indicating if HTTP/2 requests should be supported by
            the connection pool. Defaults to False.
        retries: The maximum number of retries when trying to establish a
            connection.
        local_address: Local address to connect from. Can also be used to connect
            using a particular address family. Using `local_address="0.0.0.0"`
            will connect using an `AF_INET` address (IPv4), while using
            `local_address="::"` will connect using an `AF_INET6` address (IPv6).
        uds: Path to a Unix Domain Socket to use instead of TCP sockets.
        network_backend: A backend instance to use for handling network I/O.
        socket_options: Socket options that have to be included
         in the TCP socket when the connection was established.
    """
    self._ssl_context = ssl_context

    self._max_connections = (
        sys.maxsize if max_connections is None else max_connections
    )
    self._max_keepalive_connections = (
        sys.maxsize
        if max_keepalive_connections is None
        else max_keepalive_connections
    )
    self._max_keepalive_connections = min(
        self._max_connections, self._max_keepalive_connections
    )

    self._keepalive_expiry = keepalive_expiry
    self._http1 = http1
    self._http2 = http2
    self._retries = retries
    self._local_address = local_address
    self._uds = uds

    self._network_backend = (
        SyncBackend() if network_backend is None else network_backend
    )
    self._socket_options = socket_options

    # The mutable state on a connection pool is the queue of incoming requests,
    # and the set of connections that are servicing those requests.
    self._connections: List[ConnectionInterface] = []
    self._requests: List[PoolRequest] = []

    # We only mutate the state of the connection pool within an 'optional_thread_lock'
    # context. This holds a threading lock unless we're running in async mode,
    # in which case it is a no-op.
    self._optional_thread_lock = ThreadLock()

handle_request(self, request)

Send an HTTP request, and return an HTTP response.

This is the core implementation that is called into by .request() or .stream().

Source code in httpcore/__init__.py
def handle_request(self, request: Request) -> Response:
    """
    Send an HTTP request, and return an HTTP response.

    This is the core implementation that is called into by `.request()` or `.stream()`.
    """
    scheme = request.url.scheme.decode()
    if scheme == "":
        raise UnsupportedProtocol(
            "Request URL is missing an 'http://' or 'https://' protocol."
        )
    if scheme not in ("http", "https", "ws", "wss"):
        raise UnsupportedProtocol(
            f"Request URL has an unsupported protocol '{scheme}://'."
        )

    timeouts = request.extensions.get("timeout", {})
    timeout = timeouts.get("pool", None)

    with self._optional_thread_lock:
        # Add the incoming request to our request queue.
        pool_request = PoolRequest(request)
        self._requests.append(pool_request)

    try:
        while True:
            with self._optional_thread_lock:
                # Assign incoming requests to available connections,
                # closing or creating new connections as required.
                closing = self._assign_requests_to_connections()
            self._close_connections(closing)

            # Wait until this request has an assigned connection.
            connection = pool_request.wait_for_connection(timeout=timeout)

            try:
                # Send the request on the assigned connection.
                response = connection.handle_request(
                    pool_request.request
                )
            except ConnectionNotAvailable:
                # In some cases a connection may initially be available to
                # handle a request, but then become unavailable.
                #
                # In this case we clear the connection and try again.
                pool_request.clear_connection()
            else:
                break  # pragma: nocover

    except BaseException as exc:
        with self._optional_thread_lock:
            # For any exception or cancellation we remove the request from
            # the queue, and then re-assign requests to connections.
            self._requests.remove(pool_request)
            closing = self._assign_requests_to_connections()

        self._close_connections(closing)
        raise exc from None

    # Return the response. Note that in this case we still have to manage
    # the point at which the response is closed.
    assert isinstance(response.stream, Iterable)
    return Response(
        status=response.status,
        headers=response.headers,
        content=PoolByteStream(
            stream=response.stream, pool_request=pool_request, pool=self
        ),
        extensions=response.extensions,
    )