Integration7 min read

HTTPX async proxies: setup and PoolTimeout diagnosis

Configure an HTTPX async proxy client, close streamed responses correctly, and reproduce a PoolTimeout locally before changing your proxy settings.

On this page

Use one httpx.AsyncClient for a group of requests, set its proxy= explicitly, and close each streamed response when its work ends. If the error is PoolTimeout, first inspect who owns the client's connections: that exception means a request could not acquire a connection within its pool-wait limit. It does not, by itself, show that the proxy failed. HTTPX async support, timeout phases.

This guide includes a runnable example and a local failure demonstration. The demonstration holds two responses open, observes a third request fail before it reaches the proxy, then closes one response and verifies that another request succeeds. That gives you a concrete way to separate connection ownership from remote network trouble.

Start with an explicit client

The examples require Python 3.11 or later and pin HTTPX 0.28.1. The recorded run used Python 3.14.7 and HTTPcore 1.0.9; the download contains the complete dependency pins. HTTPX 0.28 removed the older proxies= argument: use singular proxy= for this configuration. Tagged HTTPX changelog.

The reusable client factory in the download is:

python
import httpx


def make_client(proxy_url: str) -> httpx.AsyncClient:
    return httpx.AsyncClient(
        proxy=proxy_url,
        trust_env=False,
        http2=False,
        follow_redirects=False,
        limits=httpx.Limits(max_connections=2, max_keepalive_connections=2),
        timeout=httpx.Timeout(connect=5.0, read=5.0, write=5.0, pool=0.25),
    )

The small two-connection pool makes this exercise easy to inspect; it is not a recommended production capacity. HTTPX's connection limit and keep-alive limit control different things. max_connections bounds the pool's connections, while max_keepalive_connections bounds retained idle connections. A connection limit also does not bound how many application tasks you create. HTTPX resource limits.

The complete async_proxy_check.py example sends exactly three GETs through one client. A semaphore admits at most two at once. Each request has a ten-second operation deadline starting before that admission wait, as well as the client timeouts above. Responses are read inside async with client.stream(...), and reading stops with BodyTooLarge if the decoded body exceeds 65,536 bytes. The byte cap limits accepted response content; it is not a hard limit on decompressor memory. These are deliberate limits for a small diagnostic, not workload sizing advice.

There is no automatic retry loop. Keep the same client around for the work it owns instead of creating a new client inside every request task. Context-managed streams close on exit; manual client.send(..., stream=True) calls make response closure your responsibility. HTTPX client and stream lifetime.

Keep the proxy connection separate from the destination

An HTTPS destination does not automatically require an https:// proxy address. With an HTTP proxy, the client can request a CONNECT tunnel and then negotiate TLS with the HTTPS destination through that tunnel. Use the proxy scheme and authentication method your provider documents. The local experiment below exercises plain HTTP forwarding, not CONNECT, TLS, SOCKS, authentication, or a provider's service. HTTPX proxy configuration.

For this baseline, proxy= selects the route and trust_env=False keeps inherited environment configuration out of the client. In HTTPX 0.28.1, do not expect NO_PROXY to override an explicitly supplied proxy=. If you need route exceptions, configure and verify them deliberately rather than assuming the environment bypassed this client. Tagged client routing.

There is a certificate consequence too: trust_env=False disables HTTPX's use of SSL_CERT_FILE and SSL_CERT_DIR. The example keeps normal certificate verification enabled. If your deployment requires a private CA, configure an explicit trusted SSLContext as described in the HTTPX SSL guide; do not fix a trust error with verify=False. See also HTTPX environment variables.

Reproduce the pool failure without a proxy account

Download and extract the HTTPX async proxy example. From the extracted directory, run:

sh
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
python pool_timeout_demo.py

Dependency installation uses the package index. The demonstration itself creates a temporary HTTP origin and forward proxy on loopback and sends no requests to an external destination. It uses the same client factory shown above, with HTTP/1.1 and a two-connection pool.

The important result is the request sequence, not a speed score:

StepClient observationProxy and origin observation
Open /hold/1 and /hold/2 as manual streamsBoth response bodies remain unfinishedBoth paths have arrived
Request /blocked with the pool fullPoolTimeout/blocked has not arrived at either hop
Call aclose() on the first held responseOne occupied connection is releasedThe second response remains deliberately open
Request /okHTTP 200 with the expected JSON body/ok arrives at both hops

The recorded run produced exactly /hold/1, /hold/2, and /ok at the proxy and at the origin. Its cleanup checks reported no outstanding fixture handlers or writers. This is an executed local behavior demonstration; it does not measure an external proxy's latency, availability, or throughput.

The recovered response matters because it shows the client can make progress without changing the proxy, destination, or connection limit. The absence of /blocked at the proxy gives a second observation of where this particular attempt stopped. Do not generalize that trace into a claim that every pool timeout is a leak: legitimate long streams and more concurrent work than available connections can also create a wait.

Fix ownership before adding capacity

For normal streaming work, keep the response lifetime inside a context manager. The downloaded checker owns both the response and its bounded read loop; its result is returned only after the stream has been consumed or the operation has failed. For manual streaming, close the response with await response.aclose() on every exit path, including exceptions and cancellation. HTTPX streaming documentation.

Closing a response releases its resources; it does not promise that a partially consumed HTTP/1.1 connection will be reusable. The next request may need a new connection. If every open stream still has useful work, reducing admitted concurrency or selecting a larger bounded pool may be appropriate. If abandoned responses still own connections, a larger pool postpones the same problem. Tagged response closure, HTTPcore connection-pool ownership.

Also distinguish a quiet connection from a slow complete operation. An HTTPX read timeout limits a wait for a data chunk; it is not a deadline for downloading an entire slowly arriving response. The checker adds a separate whole-operation deadline. A production queue also needs its own admission, memory, cancellation, and shutdown policy; this three-request demonstration is not a complete worker system. HTTPX timeouts, Python operation deadlines.

Check your own route with the complete example

Have your environment or secret manager provide PROXY_URL and an authorized TARGET_URL, then run:

sh
python async_proxy_check.py

This sends three GETs. Choose a small endpoint you control or are allowed to test; avoid a URL whose GET operation triggers work you do not intend to repeat. The checker prints status, decoded byte count, and a SHA-256 digest on success, or an error class on failure. It does not print the URLs, response bodies, or raw exception messages, which may contain sensitive data.

A matching digest only shows that the observed bodies match. It does not prove that they contain useful content: a repeated challenge page can also have a stable digest. The local demonstration separately checks its expected JSON body. Add the equivalent content check for your destination before treating your own route as successful.

Use the result to choose the next investigation:

  • PoolTimeout: inspect open response ownership, concurrent work, and pool-wait limits first. Use a trace like the local example to determine whether an attempt reached the proxy.
  • Connection or proxy errors: investigate the configured gateway and the failed stage. A proxy exception and a destination HTTP status are different observations.
  • ReadTimeout or OperationDeadline: find what was still waiting when the limit expired. The whole-operation deadline also includes time waiting for admission.
  • HTTPStatusError or BodyTooLarge: inspect the response policy and the expected resource. Increasing the connection pool does not change either check.

The network timeout guide covers DNS, TCP, CONNECT, TLS and body-stage diagnosis. The environment-variable guide compares routing behavior across other clients. For synchronous Python code, use the separate Requests proxy guide.

Downloads and method

The archive includes the complete checker, loopback demonstration, dependency pins, test suite and README. Individual files are also available: checker, demonstration, requirements, tests, README, and recorded output.

Method: ipvolt ran controlled loopback checks on 13 September 2026 using the pinned versions above. The exercised cases cover pool exhaustion and recovery, the complete checker, HTTP-status and oversized-body failures, and cancellation cleanup. The fixture sends Connection: close, so it demonstrates reuse of a client and release of pool capacity, not TCP keep-alive reuse. These checks do not establish behavior for every Python version, operating system, HTTP/2 server, authentication scheme, or proxy product.

If you want to hear when ipvolt access opens, join the early-access list. One email when access opens. Nothing else. These examples are client diagnostics, not documentation of an available ipvolt endpoint.

Sources & further reading

Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.