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 defaulthttpcore.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 toTrue
.http2
: A boolean indicating if HTTP/2 requests should be supported by the connection pool. Defaults toFalse
.
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. Usinglocal_address="0.0.0.0"
will connect using anAF_INET
address (IPv4), while usinglocal_address="::"
will connect using anAF_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[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, proxy=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 |
ssl.SSLContext | None |
An SSL context to use for verifying connections.
If not specified, the default |
None |
max_connections |
int | None |
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 |
int | None |
The maximum number of idle HTTP connections that will be maintained in the pool. |
None |
keepalive_expiry |
float | None |
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 |
str | None |
Local address to connect from. Can also be used to connect
using a particular address family. Using |
None |
uds |
str | None |
Path to a Unix Domain Socket to use instead of TCP sockets. |
None |
network_backend |
NetworkBackend | None |
A backend instance to use for handling network I/O. |
None |
socket_options |
Iterable[SOCKET_OPTION] | None |
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: ssl.SSLContext | None = None,
proxy: Proxy | None = None,
max_connections: int | None = 10,
max_keepalive_connections: int | None = None,
keepalive_expiry: float | None = None,
http1: bool = True,
http2: bool = False,
retries: int = 0,
local_address: str | None = None,
uds: str | None = None,
network_backend: NetworkBackend | None = None,
socket_options: typing.Iterable[SOCKET_OPTION] | None = 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._proxy = proxy
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, typing.Iterable)
return Response(
status=response.status,
headers=response.headers,
content=PoolByteStream(
stream=response.stream, pool_request=pool_request, pool=self
),
extensions=response.extensions,
)