<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Threading - Tag - fds-board</title><link>https://blog.fdsboard.com/tags/threading/</link><description>Threading - Tag - fds-board</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 14 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.fdsboard.com/tags/threading/" rel="self" type="application/rss+xml"/><item><title>332 Threads, 164 Arenas: Debugging Thread and Memory Leaks in a Python asyncio Service</title><link>https://blog.fdsboard.com/python/thread-leak-asyncio-executor/</link><pubDate>Sat, 14 Mar 2026 00:00:00 +0000</pubDate><author>Jinze(Jerry) Zhou</author><guid>https://blog.fdsboard.com/python/thread-leak-asyncio-executor/</guid><description><![CDATA[<h2 id="1-summary">1. Summary</h2>
<p>A long-running Python 3.12 service leaked 32 OS threads every processing cycle, and even after
that got fixed, RSS kept climbing — while every Python-level profiler (<code>tracemalloc</code>,
<code>pympler</code>) reported a clean heap. The first bug was structural: nested <code>asyncio.run()</code> calls
each spin up a fresh default <code>ThreadPoolExecutor</code>, and fire-and-forget tasks race against that
executor&rsquo;s shutdown, so threads survive cleanup. The second bug was one layer below Python
entirely: glibc assigns each new thread to its own memory arena, and short-lived thread pools
leave those arenas — and the committed memory inside them — behind forever. The third bug was a
footgun in how you fix the second: <code>os.environ.setdefault('MALLOC_ARENA_MAX', ...)</code> silently
does nothing, because glibc reads that variable once, before the Python process even starts.
None of these three failure modes are specific to this service — they generalize to any
long-running process that spawns threads through nested event loops or executors.</p>]]></description></item></channel></rss>