# HTTPX async proxies: setup and PoolTimeout diagnosis

Source: https://ipvolt.com/guides/httpx-async-proxy
Markdown: https://ipvolt.com/guides/httpx-async-proxy.md

[Home](https://ipvolt.com/index.md) / [Guides](https://ipvolt.com/guides.md) / [HTTPX async proxies: setup and PoolTimeout diagnosis](https://ipvolt.com/guides/httpx-async-proxy.md)

Category: Integration
Reviewed: 2026-09-13
Published: 2026-09-13
Reading time: 7 minutes
Author: ipvolt

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

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](https://www.python-httpx.org/async/), [timeout phases](https://www.python-httpx.org/advanced/timeouts/).

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](https://raw.githubusercontent.com/encode/httpx/0.28.1/CHANGELOG.md).

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](https://www.python-httpx.org/advanced/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](https://www.python-httpx.org/async/).

## 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](https://www.python-httpx.org/advanced/proxies/).

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](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_client.py).

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](https://www.python-httpx.org/advanced/ssl/); do not fix a trust error with `verify=False`. See also [HTTPX environment variables](https://www.python-httpx.org/environment_variables/).

## Reproduce the pool failure without a proxy account

Download and extract the [HTTPX async proxy example](https://ipvolt.com/downloads/httpx-async-proxy/httpx-async-proxy.zip). 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:

| Step | Client observation | Proxy and origin observation |
|---|---|---|
| Open `/hold/1` and `/hold/2` as manual streams | Both response bodies remain unfinished | Both paths have arrived |
| Request `/blocked` with the pool full | `PoolTimeout` | `/blocked` has not arrived at either hop |
| Call `aclose()` on the first held response | One occupied connection is released | The second response remains deliberately open |
| Request `/ok` | HTTP 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](https://www.python-httpx.org/async/).

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](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_models.py), [HTTPcore connection-pool ownership](https://raw.githubusercontent.com/encode/httpcore/1.0.9/httpcore/_async/connection_pool.py).

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](https://www.python-httpx.org/advanced/timeouts/), [Python operation deadlines](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout).

## 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](https://ipvolt.com/guides/proxy-timeout-troubleshooting) covers DNS, TCP, CONNECT, TLS and body-stage diagnosis. The [environment-variable guide](https://ipvolt.com/guides/proxy-environment-variables) compares routing behavior across other clients. For synchronous Python code, use the separate [Requests proxy guide](https://ipvolt.com/guides/python-requests-proxy).

## Downloads and method

The [archive](https://ipvolt.com/downloads/httpx-async-proxy/httpx-async-proxy.zip) includes the complete checker, loopback demonstration, dependency pins, test suite and README. Individual files are also available: [checker](https://ipvolt.com/downloads/httpx-async-proxy/async_proxy_check.py), [demonstration](https://ipvolt.com/downloads/httpx-async-proxy/pool_timeout_demo.py), [requirements](https://ipvolt.com/downloads/httpx-async-proxy/requirements.txt), [tests](https://ipvolt.com/downloads/httpx-async-proxy/test_httpx_proxy.py), [README](https://ipvolt.com/downloads/httpx-async-proxy/README.md), and [recorded output](https://ipvolt.com/downloads/httpx-async-proxy/example-output.json).

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](https://ipvolt.com/#waitlist-closing). One email when access opens. Nothing else. These examples are client diagnostics, not documentation of an available ipvolt endpoint.

## Sources & further reading

- [HTTPX async support](https://www.python-httpx.org/async/)
- [timeout phases](https://www.python-httpx.org/advanced/timeouts/)
- [Tagged HTTPX changelog](https://raw.githubusercontent.com/encode/httpx/0.28.1/CHANGELOG.md)
- [HTTPX resource limits](https://www.python-httpx.org/advanced/resource-limits/)
- [HTTPX proxy configuration](https://www.python-httpx.org/advanced/proxies/)
- [Tagged client routing](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_client.py)
- [HTTPX SSL guide](https://www.python-httpx.org/advanced/ssl/)
- [HTTPX environment variables](https://www.python-httpx.org/environment_variables/)
- [Tagged response closure](https://raw.githubusercontent.com/encode/httpx/0.28.1/httpx/_models.py)
- [HTTPcore connection-pool ownership](https://raw.githubusercontent.com/encode/httpcore/1.0.9/httpcore/_async/connection_pool.py)
- [Python operation deadlines](https://docs.python.org/3/library/asyncio-task.html#asyncio.timeout)

## Related guides

- [Troubleshoot proxy timeouts one stage at a time](https://ipvolt.com/guides/proxy-timeout-troubleshooting.md)
- [Proxy environment variables: HTTP_PROXY and NO_PROXY](https://ipvolt.com/guides/proxy-environment-variables.md)
- [Configure a proxy in Python Requests](https://ipvolt.com/guides/python-requests-proxy.md)

## About ipvolt

Examples use generic proxy settings, with links to the original technical documentation. Product-specific behavior must be checked with your provider. ipvolt is still in development.

## Know when access opens.

ipvolt · In development

We’re building proxy infrastructure for developers and data teams. Join the interest list for a heads-up when ipvolt is ready.

Consent: One email when access opens. Nothing else.

[Get early access](https://ipvolt.com/guides/httpx-async-proxy#waitlist-closing). Use the email form on this page to join the interest list.

[Privacy](https://ipvolt.com/privacy)

