tomchristie: The URLLib3 changes here will become redundant after #966, but I'm going to ignore that here to keep this PR tightly scoped.
tomchristie: Merged #967 into this as we're dependant on it.
diff --git a/docs/advanced.md b/docs/advanced.md
index 4d1f1d8..2c1f1a6 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -404,14 +404,14 @@ response = client.get('http://example.com/')
 You can control the connection pool size using the `pool_limits` keyword
 argument on the client. It takes instances of `httpx.PoolLimits` which define:
 
-- `soft_limit`, number of allowable keep-alive connections, or `None` to always
+- `max_keepalive`, number of allowable keep-alive connections, or `None` to always
 allow. (Defaults 10)
-- `hard_limit`, maximum number of allowable connections, or` None` for no limits.
+- `max_connections`, maximum number of allowable connections, or` None` for no limits.
 (Default 100)
 
 
 ```python
-limits = httpx.PoolLimits(soft_limit=5, hard_limit=10)
+limits = httpx.PoolLimits(max_keepalive=5, max_connections=10)
 client = httpx.Client(pool_limits=limits)
 ```
 
diff --git a/httpx/_client.py b/httpx/_client.py
index 8c3e47f..a526a0c 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -504,13 +504,11 @@ class Client(BaseClient):
         ssl_context = SSLConfig(
             verify=verify, cert=cert, trust_env=trust_env
         ).ssl_context
-        max_keepalive = pool_limits.soft_limit
-        max_connections = pool_limits.hard_limit
 
         return httpcore.SyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=max_keepalive,
-            max_connections=max_connections,
+            max_keepalive=pool_limits.max_keepalive,
+            max_connections=pool_limits.max_connections,
         )
 
     def init_proxy_transport(
@@ -524,16 +522,14 @@ class Client(BaseClient):
         ssl_context = SSLConfig(
             verify=verify, cert=cert, trust_env=trust_env
         ).ssl_context
-        max_keepalive = pool_limits.soft_limit
-        max_connections = pool_limits.hard_limit
 
         return httpcore.SyncHTTPProxy(
             proxy_url=proxy.url.raw,
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=max_keepalive,
-            max_connections=max_connections,
+            max_keepalive=pool_limits.max_keepalive,
+            max_connections=pool_limits.max_connections,
         )
 
     def transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
@@ -1050,13 +1046,11 @@ class AsyncClient(BaseClient):
         ssl_context = SSLConfig(
             verify=verify, cert=cert, trust_env=trust_env
         ).ssl_context
-        max_keepalive = pool_limits.soft_limit
-        max_connections = pool_limits.hard_limit
 
         return httpcore.AsyncConnectionPool(
             ssl_context=ssl_context,
-            max_keepalive=max_keepalive,
-            max_connections=max_connections,
+            max_keepalive=pool_limits.max_keepalive,
+            max_connections=pool_limits.max_connections,
             http2=http2,
         )
 
@@ -1072,16 +1066,14 @@ class AsyncClient(BaseClient):
         ssl_context = SSLConfig(
             verify=verify, cert=cert, trust_env=trust_env
         ).ssl_context
-        max_keepalive = pool_limits.soft_limit
-        max_connections = pool_limits.hard_limit
 
         return httpcore.AsyncHTTPProxy(
             proxy_url=proxy.url.raw,
             proxy_headers=proxy.headers.raw,
             proxy_mode=proxy.mode,
             ssl_context=ssl_context,
-            max_keepalive=max_keepalive,
-            max_connections=max_connections,
+            max_keepalive=pool_limits.max_keepalive,
+            max_connections=pool_limits.max_connections,
             http2=http2,
         )
 
diff --git a/httpx/_config.py b/httpx/_config.py
index 1247882..0dc7a46 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -8,7 +8,7 @@ import certifi
 
 from ._models import URL, Headers
 from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes
-from ._utils import get_ca_bundle_from_env, get_logger
+from ._utils import get_ca_bundle_from_env, get_logger, warn_deprecated
 
 DEFAULT_CIPHERS = ":".join(
     [
@@ -288,29 +288,43 @@ class PoolLimits:
 
     **Parameters:**
 
-    * **soft_limit** - Allow the connection pool to maintain keep-alive connections
+    * **max_keepalive** - Allow the connection pool to maintain keep-alive connections
                        below this point.
-    * **hard_limit** - The maximum number of concurrent connections that may be
+    * **max_connections** - The maximum number of concurrent connections that may be
                        established.
     """
 
     def __init__(
-        self, *, soft_limit: int = None, hard_limit: int = None,
+        self,
+        *,
+        max_keepalive: int = None,
+        max_connections: int = None,
+        soft_limit: int = None,
+        hard_limit: int = None,
     ):
-        self.soft_limit = soft_limit
-        self.hard_limit = hard_limit
+        self.max_keepalive = max_keepalive
+        self.max_connections = max_connections
+        if soft_limit is not None:  # pragma: nocover
+            self.max_keepalive = soft_limit
+            warn_deprecated("'soft_limit' is deprecated. Use 'max_keepalive' instead.",)
+        if hard_limit is not None:  # pragma: nocover
+            self.max_connections = hard_limit
+            warn_deprecated(
+                "'hard_limit' is deprecated. Use 'max_connections' instead.",
+            )
 
     def __eq__(self, other: typing.Any) -> bool:
         return (
             isinstance(other, self.__class__)
-            and self.soft_limit == other.soft_limit
-            and self.hard_limit == other.hard_limit
+            and self.max_keepalive == other.max_keepalive
+            and self.max_connections == other.max_connections
         )
 
     def __repr__(self) -> str:
         class_name = self.__class__.__name__
         return (
-            f"{class_name}(soft_limit={self.soft_limit}, hard_limit={self.hard_limit})"
+            f"{class_name}(max_keepalive={self.max_keepalive}, "
+            f"max_connections={self.max_connections})"
         )
 
 
diff --git a/httpx/_transports/urllib3.py b/httpx/_transports/urllib3.py
index 71882e1..714c925 100644
--- a/httpx/_transports/urllib3.py
+++ b/httpx/_transports/urllib3.py
@@ -34,23 +34,23 @@ class URLLib3Transport(httpcore.SyncHTTPTransport):
         ssl_config = SSLConfig(
             verify=verify, cert=cert, trust_env=trust_env, http2=False
         )
-        hard_limit = pool_limits.hard_limit
-        soft_limit = pool_limits.soft_limit
+        max_connections = pool_limits.max_connections
+        max_keepalive = pool_limits.max_keepalive
 
         # Our connection pool configuration doesn't quite match up with urllib3's
         # controls, but we can put sensible mappings in place:
-        if hard_limit is None:
+        if max_connections is None:
             block = False
-            if soft_limit is None:
+            if max_keepalive is None:
                 num_pools = 1000
                 maxsize = 1000
             else:
-                num_pools = int(math.sqrt(soft_limit))
-                maxsize = int(math.sqrt(soft_limit))
+                num_pools = int(math.sqrt(max_keepalive))
+                maxsize = int(math.sqrt(max_keepalive))
         else:
             block = True
-            num_pools = int(math.sqrt(hard_limit))
-            maxsize = int(math.sqrt(hard_limit))
+            num_pools = int(math.sqrt(max_connections))
+            maxsize = int(math.sqrt(max_connections))
 
         self.pool = self.init_pool_manager(
             proxy=proxy,

Deprecate ProxyLimits naming in favour of max_connections and max_keepalive
Prompted by https://github.com/encode/httpx/pull/942#discussion_r424341967

The `PoolLimits` class uses `soft_limit` and `hard_limit` whereas in httpcore we use `max_connections` and `max_keepalive` which are clearer.
diff --git a/tests/test_config.py b/tests/test_config.py
index f0ecf31..41d8191 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -118,13 +118,13 @@ def test_ssl_eq():
 
 
 def test_limits_repr():
-    limits = httpx.PoolLimits(hard_limit=100)
-    assert repr(limits) == "PoolLimits(soft_limit=None, hard_limit=100)"
+    limits = httpx.PoolLimits(max_connections=100)
+    assert repr(limits) == "PoolLimits(max_keepalive=None, max_connections=100)"
 
 
 def test_limits_eq():
-    limits = httpx.PoolLimits(hard_limit=100)
-    assert limits == httpx.PoolLimits(hard_limit=100)
+    limits = httpx.PoolLimits(max_connections=100)
+    assert limits == httpx.PoolLimits(max_connections=100)
 
 
 def test_timeout_eq():

pip install -e .
parse_log_pytest
requirements.txt
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aspy.refactor-imports==3.0.2
async-generator==1.10
attrs==25.3.0
autoflake==2.3.1
babel==2.17.0
backports.tarfile==1.2.0
backrefs==5.8
black==25.1.0
brotlipy==0.7.0
certifi==2025.1.31
cffi==1.17.1
chardet==3.0.4
charset-normalizer==3.4.1
click==8.1.8
colorama==0.4.6
coverage==7.8.0
cryptography==44.0.2
docutils==0.21.2
exceptiongroup==1.2.2
flake8==7.2.0
flake8-bugbear==24.12.12
flake8-pie==0.16.0
ghp-import==2.1.0
h11==0.9.0
h2==3.2.0
hpack==3.0.0
hstspreload==2025.1.1
httpcore==0.9.1
-e git+https://github.com/encode/httpx.git@9b6605c3d519142db7ebaf56c13094bf779c7dea#egg=httpx
hyperframe==5.2.0
id==1.5.0
idna==2.10
importlib_metadata==8.6.1
iniconfig==2.1.0
isort==6.0.1
jaraco.classes==3.4.0
jaraco.context==6.0.1
jaraco.functools==4.1.0
jeepney==0.9.0
Jinja2==3.1.6
keyring==25.6.0
Markdown==3.7
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mccabe==0.7.0
mdurl==0.1.2
mergedeep==1.3.4
mkautodoc==0.2.0
mkdocs==1.6.1
mkdocs-get-deps==0.2.0
mkdocs-material==9.6.10
mkdocs-material-extensions==1.3.1
more-itertools==10.6.0
mypy==1.15.0
mypy-extensions==1.0.0
nh3==0.2.21
outcome==1.3.0.post0
packaging==24.2
paginate==0.5.7
pathspec==0.12.1
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.2
Pygments==2.19.1
pymdown-extensions==10.14.3
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-trio==0.8.0
python-dateutil==2.9.0.post0
PyYAML==6.0.2
pyyaml_env_tag==0.1
readme_renderer==44.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==1.5.0
rich==14.0.0
SecretStorage==3.3.3
seed-isort-config==2.2.0
six==1.17.0
sniffio==1.3.1
sortedcontainers==2.4.0
tomli==2.2.1
trio==0.29.0
trio-typing==0.10.0
trustme==1.2.1
twine==6.1.0
typing_extensions==4.13.0
urllib3==2.3.0
uvicorn==0.34.0
watchdog==6.0.0
zipp==3.21.0

name: httpx
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aspy-refactor-imports==3.0.2
      - async-generator==1.10
      - attrs==25.3.0
      - autoflake==2.3.1
      - babel==2.17.0
      - backports-tarfile==1.2.0
      - backrefs==5.8
      - black==25.1.0
      - brotlipy==0.7.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==3.0.4
      - charset-normalizer==3.4.1
      - click==8.1.8
      - colorama==0.4.6
      - coverage==7.8.0
      - cryptography==44.0.2
      - docutils==0.21.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - flake8-bugbear==24.12.12
      - flake8-pie==0.16.0
      - ghp-import==2.1.0
      - h11==0.9.0
      - h2==3.2.0
      - hpack==3.0.0
      - hstspreload==2025.1.1
      - httpcore==0.9.1
      - hyperframe==5.2.0
      - id==1.5.0
      - idna==2.10
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isort==6.0.1
      - jaraco-classes==3.4.0
      - jaraco-context==6.0.1
      - jaraco-functools==4.1.0
      - jeepney==0.9.0
      - jinja2==3.1.6
      - keyring==25.6.0
      - markdown==3.7
      - markdown-it-py==3.0.0
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mdurl==0.1.2
      - mergedeep==1.3.4
      - mkautodoc==0.2.0
      - mkdocs==1.6.1
      - mkdocs-get-deps==0.2.0
      - mkdocs-material==9.6.10
      - mkdocs-material-extensions==1.3.1
      - more-itertools==10.6.0
      - mypy==1.15.0
      - mypy-extensions==1.0.0
      - nh3==0.2.21
      - outcome==1.3.0.post0
      - packaging==24.2
      - paginate==0.5.7
      - pathspec==0.12.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.2
      - pygments==2.19.1
      - pymdown-extensions==10.14.3
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-trio==0.8.0
      - python-dateutil==2.9.0.post0
      - pyyaml==6.0.2
      - pyyaml-env-tag==0.1
      - readme-renderer==44.0
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - rfc3986==1.5.0
      - rich==14.0.0
      - secretstorage==3.3.3
      - seed-isort-config==2.2.0
      - six==1.17.0
      - sniffio==1.3.1
      - sortedcontainers==2.4.0
      - tomli==2.2.1
      - trio==0.29.0
      - trio-typing==0.10.0
      - trustme==1.2.1
      - twine==6.1.0
      - typing-extensions==4.13.0
      - urllib3==2.3.0
      - uvicorn==0.34.0
      - watchdog==6.0.0
      - zipp==3.21.0
prefix: /opt/conda/envs/httpx

tests/test_config.py::test_limits_repr tests/test_config.py::test_limits_eq
tests/test_config.py::test_load_ssl_config tests/test_config.py::test_load_ssl_config_verify_non_existing_path tests/test_config.py::test_load_ssl_config_verify_existing_file tests/test_config.py::test_load_ssl_config_verify_env_file[SSL_CERT_FILE] tests/test_config.py::test_load_ssl_config_verify_env_file[SSL_CERT_DIR] tests/test_config.py::test_load_ssl_config_verify_directory tests/test_config.py::test_load_ssl_config_cert_and_key tests/test_config.py::test_load_ssl_config_cert_and_encrypted_key[password0] tests/test_config.py::test_load_ssl_config_cert_and_encrypted_key[password1] tests/test_config.py::test_load_ssl_config_cert_and_key_invalid_password tests/test_config.py::test_load_ssl_config_cert_without_key_raises tests/test_config.py::test_load_ssl_config_no_verify tests/test_config.py::test_load_ssl_context tests/test_config.py::test_ssl_repr tests/test_config.py::test_ssl_eq tests/test_config.py::test_timeout_eq tests/test_config.py::test_timeout_from_nothing tests/test_config.py::test_timeout_from_none tests/test_config.py::test_timeout_from_one_none_value tests/test_config.py::test_timeout_from_one_value tests/test_config.py::test_timeout_from_one_value_and_default tests/test_config.py::test_timeout_from_tuple tests/test_config.py::test_timeout_from_config_instance tests/test_config.py::test_timeout_repr tests/test_config.py::test_ssl_config_support_for_keylog_file tests/test_config.py::test_proxy_from_url[https://example.com-https://example.com-expected_headers0-DEFAULT] tests/test_config.py::test_proxy_from_url[https://user:pass@example.com-https://example.com:443-expected_headers1-DEFAULT] tests/test_config.py::test_invalid_proxy_scheme tests/test_config.py::test_invalid_proxy_mode
httpx/_transports/urllib3.py httpx/_config.py docs/advanced.md httpx/_client.py
httpx/_transports/urllib3.py httpx/_config.py docs/advanced.md httpx/_client.py