From b8e13bd421d37c3f06c59a5ac5c50e051785ad9d Mon Sep 17 00:00:00 2001 From: vercingetorx <40043405+vercingetorx@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:11:18 -0800 Subject: [PATCH] Fix memory leak in asyncdispatch.withTimeout by clearing losing callbacks (#25567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit withTimeout currently leaves the “losing” callback installed: - when fut finishes first, timeout callback remains until timer fires, - when timeout fires first, fut callback remains on the wrapped future. Under high-throughput use with large future payloads, this retains closures/future references longer than needed and causes large transient RSS growth. This patch clears the opposite callback immediately once outcome is decided, reducing retention without changing API behavior. (cherry picked from commit 9ed4077d9a57e19063c1c67710fa6fb83f5a5fb7) --- lib/pure/asyncdispatch.nim | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/pure/asyncdispatch.nim b/lib/pure/asyncdispatch.nim index 004cc9bcfe..70d94b023e 100644 --- a/lib/pure/asyncdispatch.nim +++ b/lib/pure/asyncdispatch.nim @@ -1946,9 +1946,14 @@ proc withTimeout*[T](fut: Future[T], timeout: int): owned(Future[bool]) = retFuture.fail(fut.error) else: retFuture.complete(true) + # Timeout side lost; drop its callback to avoid retaining closures/futures. + timeoutFuture.clearCallbacks() timeoutFuture.callback = proc () = - if not retFuture.finished: retFuture.complete(false) + if not retFuture.finished: + retFuture.complete(false) + # Wrapped future side lost; drop its callback to avoid retaining closures/futures. + fut.clearCallbacks() return retFuture proc accept*(socket: AsyncFD,