From a2ab5721168a6ed4317babc909d0cb6fcd0c5fda Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 18 Dec 2025 14:07:17 -0800 Subject: [PATCH 1/3] Use weakref for bound stream _response. This avoids creating reference cycles that can result in significant extra memory usage. The cyclic GC should clean up the cycles but avoiding them is better. --- httpx/_client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/httpx/_client.py b/httpx/_client.py index 13cd933673..6a39feead8 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -6,6 +6,7 @@ import time import typing import warnings +import weakref from contextlib import asynccontextmanager, contextmanager from types import TracebackType @@ -146,7 +147,7 @@ def __init__( self, stream: SyncByteStream, response: Response, start: float ) -> None: self._stream = stream - self._response = response + self._response = weakref.ref(response) self._start = start def __iter__(self) -> typing.Iterator[bytes]: @@ -155,7 +156,9 @@ def __iter__(self) -> typing.Iterator[bytes]: def close(self) -> None: elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + response = self._response() + if response is not None: + response.elapsed = datetime.timedelta(seconds=elapsed) self._stream.close() @@ -169,7 +172,7 @@ def __init__( self, stream: AsyncByteStream, response: Response, start: float ) -> None: self._stream = stream - self._response = response + self._response = weakref.ref(response) self._start = start async def __aiter__(self) -> typing.AsyncIterator[bytes]: @@ -178,7 +181,10 @@ async def __aiter__(self) -> typing.AsyncIterator[bytes]: async def aclose(self) -> None: elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + response = self._response() + if response is not None: + assert 0 + response.elapsed = datetime.timedelta(seconds=elapsed) await self._stream.aclose() From 98d65fe0941c767e9474b5da8727932f115658b3 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 19 Dec 2025 00:20:51 -0800 Subject: [PATCH 2/3] Remove unintended "assert". --- httpx/_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/httpx/_client.py b/httpx/_client.py index 6a39feead8..c4ae96ddbe 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -183,7 +183,6 @@ async def aclose(self) -> None: elapsed = time.perf_counter() - self._start response = self._response() if response is not None: - assert 0 response.elapsed = datetime.timedelta(seconds=elapsed) await self._stream.aclose() From d4d1959a6953ced4eca59bc7a35691e7561ec73c Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Mon, 22 Dec 2025 16:00:46 -0800 Subject: [PATCH 3/3] Add CHANGELOG note. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fa44b8ef..22d0ffd300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [UNRELEASED] +### Fixed + +* Avoid creating references cycles from `BoundSyncStream` and `BoundAsyncStream` to the response object. This allows memory to be freed by reference counting, which happens quickly, rather than waiting for the cyclic GC to run. + ### Removed * Drop support for Python 3.8