From 9ed4077d9a57e19063c1c67710fa6fb83f5a5fb7 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. --- 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,