<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Memory on Krishnatejaswi S</title>
    <link>https://KTS-o7.github.io/bear/tags/memory/</link>
    <description>Recent content in Memory on Krishnatejaswi S</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</managingEditor>
    <webMaster>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</webMaster>
    <copyright>Krishnatejaswi S</copyright>
    <lastBuildDate>Mon, 16 Feb 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://KTS-o7.github.io/bear/tags/memory/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Hunting OOMKilled in Kubernetes: Four Memory Leaks That Almost Took Down Our Platform</title>
      <link>https://KTS-o7.github.io/bear/infra/kubernetes_oom_memory_leaks/</link>
      <pubDate>Mon, 16 Feb 2026 00:00:00 +0000</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/infra/kubernetes_oom_memory_leaks/</guid>
      <description>&lt;p&gt;A pod restarts at 3 AM. Then again at 3:14. By 3:30 it&amp;rsquo;s in a &lt;code&gt;CrashLoopBackOff&lt;/code&gt;. The dashboard shows memory climbing in a clean line - no spikes, no sudden jumps - just a slow, relentless climb until the OOM killer steps in.&lt;/p&gt;&#xA;&lt;p&gt;This is the story of how I tracked down four distinct memory leaks across two Python services running on Kubernetes. Each one was a different flavor of &amp;ldquo;silent accumulation,&amp;rdquo; and each one taught me something about how easy it is to bleed memory in async Python without any obvious errors.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A pod restarts at 3 AM. Then again at 3:14. By 3:30 it&rsquo;s in a <code>CrashLoopBackOff</code>. The dashboard shows memory climbing in a clean line - no spikes, no sudden jumps - just a slow, relentless climb until the OOM killer steps in.</p>
<p>This is the story of how I tracked down four distinct memory leaks across two Python services running on Kubernetes. Each one was a different flavor of &ldquo;silent accumulation,&rdquo; and each one taught me something about how easy it is to bleed memory in async Python without any obvious errors.</p>
<p>No names. No product details. Just the bugs, the fixes, and the lessons.</p>
<h2 id="the-setup">The setup</h2>
<p>We had a multi-service platform running on Kubernetes. Two services are relevant here:</p>
<ul>
<li>
<p><strong>Service A</strong>: A document management service. It handled CRUD operations, search indexing, audit logging, and authorization precomputation. Built with FastAPI and async Python, backed by MongoDB and a vector store.</p>
</li>
<li>
<p><strong>Service B</strong>: A PDF processing service. It parsed documents, extracted text via OCR, and broke content into structured clauses. Heavy on PyMuPDF (fitz) for PDF rendering and Tesseract for OCR.</p>
</li>
</ul>
<p>Both services ran as single-container pods with resource limits. Service A had a 2Gi memory limit. Service B had 4Gi.</p>
<p>Both were getting OOMKilled. Regularly.</p>
<h2 id="the-symptoms">The symptoms</h2>
<p>Service A&rsquo;s memory graph looked like this:</p>





<pre tabindex="0"><code>Memory (MB)
2048 |                                              ╱ OOMKilled
     |                                           ╱
1536 |                                        ╱
     |                                     ╱
1024 |                                  ╱
     |                               ╱
 512 |                            ╱
     |                         ╱
   0 |________________________╱
     0h        6h        12h        18h        24h</code></pre><p>A steady climb at roughly 200MB per hour. No spikes. No correlation with traffic. Just a ramp.</p>
<p>Service B was different - it&rsquo;d spike during document processing batches and never come back down:</p>





<pre tabindex="0"><code>Memory (MB)
4096 |          ╱╲        ╱╲
     |        ╱   ╲     ╱   ╲    ╱ OOMKilled
3072 |      ╱      ╲  ╱      ╲ ╱
     |    ╱         ╲╱        ╱
2048 |  ╱                   ╱
     |╱                   ╱
1024 |                  ╱
     |               ╱
   0 |______________╱
     0h     2h     4h     6h     8h     10h</code></pre><p>Every batch pushed memory up. It came down a little, but never to baseline. Classic leak with GC partially reclaiming some objects but not the real offender.</p>
<h2 id="leak-1-the-coroutines-that-never-ran">Leak #1: The coroutines that never ran</h2>
<p>This was the most insidious one. It looked like perfectly normal Python code:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">BaseDoc</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="nf">_write_audit_log</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">old_data</span><span class="p">,</span> <span class="n">new_data</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">enqueue_audit_create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">old_data</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">old</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_write_audit_log</span><span class="p">(</span><span class="s2">&#34;update&#34;</span><span class="p">,</span> <span class="n">old</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">await</span> <span class="n">create_snapshots</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">await</span> <span class="n">insert_versions_docs</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="n">result</span></span></span></code></pre></div><p>Spot the bug?</p>
<p><code>_write_audit_log</code> calls <code>enqueue_audit_create()</code>, which is an <strong>async function</strong>. But <code>_write_audit_log</code> is not async, and it doesn&rsquo;t <code>await</code> the call. It just calls it.</p>
<p>When you call an async function without <code>await</code>, Python creates a coroutine object but never schedules it. The coroutine sits in memory, holding references to all its arguments - in this case, entire document objects with <code>old_data</code> and <code>new_data</code>.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Coroutine Object (0x7f...)
</span></span><span class="line"><span class="ln">2</span><span class="cl">  ├── State: CREATED (never scheduled)
</span></span><span class="line"><span class="ln">3</span><span class="cl">  └── Frame Object
</span></span><span class="line"><span class="ln">4</span><span class="cl">       ├── Arg: self (BaseDoc instance, 2KB)
</span></span><span class="line"><span class="ln">5</span><span class="cl">       ├── Arg: old_data (Dict, 5MB)  &lt;-- LEAKED
</span></span><span class="line"><span class="ln">6</span><span class="cl">       └── Arg: new_data (Dict, 5MB)  &lt;-- LEAKED
</span></span><span class="line"><span class="ln">7</span><span class="cl">       └── Local Vars: (captured closure scope)</span></span></code></pre></div><p>Python will eventually warn you about this (<code>RuntimeWarning: coroutine was never awaited</code>), but only if you have warnings enabled and you&rsquo;re looking at the right log. In a noisy production service running dozens of requests per second, these warnings get buried.</p>
<p>The same pattern appeared elsewhere:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># In the CRUD endpoints</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">create_document</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Document</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">enqueue_audit_create</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>       <span class="c1"># ← async, not awaited</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">qdrant_upserts</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>             <span class="c1"># ← async, not awaited</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">return</span> <span class="n">doc</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">update_document</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Document</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">enqueue_audit_update</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>       <span class="c1"># ← async, not awaited</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">qdrant_upserts</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>             <span class="c1"># ← async, not awaited</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="n">doc</span></span></span></code></pre></div><p>Each unawaited call created a coroutine holding the full document. For a service processing hundreds of documents per hour, that&rsquo;s hundreds of document-sized objects pinned in memory with no way to be garbage collected.</p>
<h3 id="the-fix">The fix</h3>
<p>Straightforward once you see it:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">BaseDoc</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">_write_audit_log</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">old_data</span><span class="p">,</span> <span class="n">new_data</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">await</span> <span class="n">enqueue_audit_create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">old_data</span><span class="p">,</span> <span class="n">new_data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">old</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">_write_audit_log</span><span class="p">(</span><span class="s2">&#34;update&#34;</span><span class="p">,</span> <span class="n">old</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">await</span> <span class="n">create_snapshots</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">await</span> <span class="n">insert_versions_docs</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">return</span> <span class="n">result</span></span></span></code></pre></div><p>And in the CRUD endpoints:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">create_document</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Document</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">await</span> <span class="n">enqueue_audit_create</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">await</span> <span class="n">qdrant_upserts</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="n">doc</span></span></span></code></pre></div><h3 id="the-lesson">The lesson</h3>
<p><strong>Unawaited coroutines are silent memory leaks.</strong> They don&rsquo;t crash. They don&rsquo;t raise. They just accumulate. And because they hold references to their arguments, they can keep large objects alive long after the request that created them is done.</p>
<p>If you&rsquo;re running async Python, add this to your linting:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># In your pytest conftest.py or startup</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kn">import</span> <span class="nn">warnings</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">warnings</span><span class="o">.</span><span class="n">filterwarnings</span><span class="p">(</span><span class="s2">&#34;error&#34;</span><span class="p">,</span> <span class="n">category</span><span class="o">=</span><span class="ne">RuntimeWarning</span><span class="p">,</span> <span class="n">message</span><span class="o">=</span><span class="s2">&#34;coroutine.*was never awaited&#34;</span><span class="p">)</span></span></span></code></pre></div><p>This turns the warning into an exception, which makes it impossible to miss.</p>
<h2 id="leak-2-creating-a-new-mongoclient-on-every-authorization-check">Leak #2: Creating a new MongoClient on every authorization check</h2>
<p>The authorization layer needed to precompute user access contexts. To do this, it queried MongoDB directly (not through the async ODM) using PyMongo&rsquo;s synchronous <code>MongoClient</code>.</p>
<p>The code looked like this:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">AuthorizationPrecompute</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="nf">get_user_permissions</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">MONGO_URI</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s2">&#34;main_db&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">permissions</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">permissions</span><span class="o">.</span><span class="n">find</span><span class="p">({</span><span class="s2">&#34;user&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">client</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="n">permissions</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">def</span> <span class="nf">get_role_policies</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">role_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">MONGO_URI</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s2">&#34;main_db&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">policies</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">policies</span><span class="o">.</span><span class="n">find</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="n">role_id</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">client</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">return</span> <span class="n">policies</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">def</span> <span class="nf">get_org_settings</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">org_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">MONGO_URI</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s2">&#34;main_db&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">settings</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">settings</span><span class="o">.</span><span class="n">find_one</span><span class="p">({</span><span class="s2">&#34;org&#34;</span><span class="p">:</span> <span class="n">org_id</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">client</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">return</span> <span class="n">settings</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="c1"># ... 10 more methods, each creating its own MongoClient</span></span></span></code></pre></div><p>Thirteen methods. Thirteen <code>MongoClient()</code> instantiations per authorization check.</p>
<p>Each <code>MongoClient</code> creates a connection pool, background SDAM monitoring threads, and internal caches. Even with <code>client.close()</code>, the cleanup isn&rsquo;t instant - Python&rsquo;s garbage collector has to reclaim the thread stacks, socket buffers, and internal data structures.</p>
<p>And if an exception happens between <code>MongoClient()</code> and <code>client.close()</code>, the client never gets closed at all. The connection pool lives on, holding threads and sockets until the GC eventually (maybe) collects them.</p>
<p>At 50 requests/second, each triggering an auth check, we were creating <strong>650 MongoClient instances per second</strong>. Most were closed, but the transient memory pressure and leaked exceptions meant we were always carrying a few hundred orphaned connection pools.</p>
<h3 id="the-fix-1">The fix</h3>
<p>One shared client with a connection pool:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">AuthorizationPrecompute</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">_client</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nd">@classmethod</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">def</span> <span class="nf">_get_client</span><span class="p">(</span><span class="bp">cls</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">if</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_client</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="bp">cls</span><span class="o">.</span><span class="n">_client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                <span class="n">MONGO_URI</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                <span class="n">maxPoolSize</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">                <span class="n">serverSelectionTimeoutMS</span><span class="o">=</span><span class="mi">5000</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">return</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_client</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">def</span> <span class="nf">get_user_permissions</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_client</span><span class="p">()[</span><span class="s2">&#34;main_db&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">permissions</span><span class="o">.</span><span class="n">find</span><span class="p">({</span><span class="s2">&#34;user&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">}))</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">def</span> <span class="nf">get_role_policies</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">role_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_client</span><span class="p">()[</span><span class="s2">&#34;main_db&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">policies</span><span class="o">.</span><span class="n">find</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="n">role_id</span><span class="p">}))</span></span></span></code></pre></div><p>Connection overhead went from ~50ms per call (TCP handshake + auth each time) to ~5ms (reused connection from pool).</p>
<h3 id="the-lesson-1">The lesson</h3>
<p><strong>Each <code>MongoClient</code> is an entire connection pool, not a single connection.</strong> Creating one per function call is like starting a new database server for every query. Always share a single client instance (or at most one per thread/worker). And if you can&rsquo;t guarantee <code>close()</code> will be called (i.e., exceptions exist), you definitely need a shared instance.</p>
<h2 id="leak-3-cache-entries-that-never-expire">Leak #3: Cache entries that never expire</h2>
<p>The authorization layer also had an in-memory cache for user access contexts:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">UserAccessContextCache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ttl_seconds</span><span class="o">=</span><span class="mi">300</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_ttl</span> <span class="o">=</span> <span class="n">ttl_seconds</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">and</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">entry</span><span class="p">[</span><span class="s2">&#34;created&#34;</span><span class="p">]</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">_ttl</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="k">return</span> <span class="n">entry</span><span class="p">[</span><span class="s2">&#34;value&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">def</span> <span class="nf">set</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s2">&#34;value&#34;</span><span class="p">:</span> <span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="s2">&#34;created&#34;</span><span class="p">:</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="p">}</span></span></span></code></pre></div><p>See the problem?</p>
<p>When <code>get()</code> finds an expired entry, it just returns <code>None</code>. It doesn&rsquo;t delete the entry. The stale data sits in the dict forever.</p>
<p>And <code>set()</code> only adds entries. Nothing ever removes them.</p>
<p>Over time, this cache grows without bound. Every unique user who hits the service gets a cache entry that is never cleaned up. The access context objects themselves can be large - they contain permission trees, role hierarchies, and org settings.</p>
<p>With 200MB of stale cache entries after a day of operation, this was a significant contributor to the memory ramp.</p>
<h3 id="the-fix-2">The fix</h3>
<p>Add proactive cleanup:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">UserAccessContextCache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ttl_seconds</span><span class="o">=</span><span class="mi">300</span><span class="p">,</span> <span class="n">cleanup_interval</span><span class="o">=</span><span class="mi">60</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_ttl</span> <span class="o">=</span> <span class="n">ttl_seconds</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup_interval</span> <span class="o">=</span> <span class="n">cleanup_interval</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_last_cleanup</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">def</span> <span class="nf">_cleanup_expired</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">now</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">if</span> <span class="n">now</span> <span class="o">-</span> <span class="bp">self</span><span class="o">.</span><span class="n">_last_cleanup</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup_interval</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_last_cleanup</span> <span class="o">=</span> <span class="n">now</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">expired_keys</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="n">k</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="k">if</span> <span class="n">now</span> <span class="o">-</span> <span class="n">v</span><span class="p">[</span><span class="s2">&#34;created&#34;</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_ttl</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">expired_keys</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">k</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup_expired</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">entry</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">if</span> <span class="n">entry</span> <span class="ow">and</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">entry</span><span class="p">[</span><span class="s2">&#34;created&#34;</span><span class="p">]</span> <span class="o">&lt;</span> <span class="bp">self</span><span class="o">.</span><span class="n">_ttl</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">            <span class="k">return</span> <span class="n">entry</span><span class="p">[</span><span class="s2">&#34;value&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1"># Delete on read if expired</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">if</span> <span class="n">key</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="k">del</span> <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="k">return</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="k">def</span> <span class="nf">set</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cleanup_expired</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">            <span class="s2">&#34;value&#34;</span><span class="p">:</span> <span class="n">value</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">            <span class="s2">&#34;created&#34;</span><span class="p">:</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="p">}</span></span></span></code></pre></div><p>Cleanup runs at most once per minute and removes all entries older than TTL.</p>
<h3 id="the-lesson-2">The lesson</h3>
<p><strong>Every in-memory cache needs an eviction strategy.</strong> If there&rsquo;s no mechanism to remove old entries, you have a memory leak with extra steps. &ldquo;TTL on read&rdquo; is not enough - if users stop requesting a key, that entry stays forever.</p>
<p>Use <code>cachetools.TTLCache</code> or similar if you don&rsquo;t want to roll your own. Better yet, if the cache is big enough to matter, put it in Redis where you can set <code>EXPIRE</code> and forget about it.</p>
<h2 id="leak-4-pymupdf-documents-that-never-closed">Leak #4: PyMuPDF documents that never closed</h2>
<p>Service B was the PDF processing service. It used PyMuPDF (<code>fitz</code>) to render pages and extract text. The endpoints looked like this:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/parse_pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">parse_pdf</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="n">UploadFile</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">content</span> <span class="o">=</span> <span class="k">await</span> <span class="n">file</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">stream</span><span class="o">=</span><span class="n">content</span><span class="p">,</span> <span class="n">filetype</span><span class="o">=</span><span class="s2">&#34;pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">pages</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">for</span> <span class="n">page_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="n">page_num</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">text</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">get_text</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">pix</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">get_pixmap</span><span class="p">(</span><span class="n">dpi</span><span class="o">=</span><span class="mi">300</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">image</span> <span class="o">=</span> <span class="n">pix</span><span class="o">.</span><span class="n">tobytes</span><span class="p">(</span><span class="s2">&#34;png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">pages</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">text</span><span class="p">,</span> <span class="s2">&#34;image&#34;</span><span class="p">:</span> <span class="n">image</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;pages&#34;</span><span class="p">:</span> <span class="n">pages</span><span class="p">}</span></span></span></code></pre></div><p>No <code>doc.close()</code>. No context manager. No <code>try/finally</code>.</p>
<p>PyMuPDF is a C library wrapper. When you call <code>fitz.open()</code>, it allocates memory in C-land for the document structure, page data, fonts, images, and rendering buffers. Python&rsquo;s garbage collector can eventually reclaim the Python wrapper object, but the C-side memory is only freed when <code>doc.close()</code> is explicitly called.</p>
<p>While the wrapper theoretically has a <code>__del__</code> method, relying on it in a high-throughput async loop is dangerous. Python&rsquo;s GC is lazy and may not run fast enough to keep up with the rate of C-memory allocation, leading to OOMs before the GC even realizes it needs to clean up.</p>
<p>For a 50-page PDF at 300 DPI, the pixmap rendering alone can consume 200–500MB of memory. If an exception occurs mid-processing (corrupt page, unsupported font), the doc handle leaks entirely.</p>
<h3 id="the-fix-3">The fix</h3>
<p>Two changes: proper resource cleanup, and parallel OCR with controlled concurrency.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/parse_pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">parse_pdf</span><span class="p">(</span><span class="n">file</span><span class="p">:</span> <span class="n">UploadFile</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">content</span> <span class="o">=</span> <span class="k">await</span> <span class="n">file</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">doc</span> <span class="o">=</span> <span class="n">fitz</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">stream</span><span class="o">=</span><span class="n">content</span><span class="p">,</span> <span class="n">filetype</span><span class="o">=</span><span class="s2">&#34;pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">pages</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">for</span> <span class="n">page_num</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">doc</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="n">page</span> <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="n">page_num</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">text</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">get_text</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="n">pix</span> <span class="o">=</span> <span class="n">page</span><span class="o">.</span><span class="n">get_pixmap</span><span class="p">(</span><span class="n">dpi</span><span class="o">=</span><span class="mi">300</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="n">image</span> <span class="o">=</span> <span class="n">pix</span><span class="o">.</span><span class="n">tobytes</span><span class="p">(</span><span class="s2">&#34;png&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="c1"># Explicitly free the pixmap</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="n">pix</span> <span class="o">=</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="n">pages</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;text&#34;</span><span class="p">:</span> <span class="n">text</span><span class="p">,</span> <span class="s2">&#34;image&#34;</span><span class="p">:</span> <span class="n">image</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;pages&#34;</span><span class="p">:</span> <span class="n">pages</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">doc</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></span></span></code></pre></div><p>This was applied to all four endpoints in the service: <code>/parse_pdf</code>, <code>/process_clauses</code>, <code>/process_clauses_sync</code>, and <code>/extract_links</code>.</p>
<p>We also added parallel strip processing for OCR (controlled by feature flags):</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">concurrent.futures</span> <span class="kn">import</span> <span class="n">ThreadPoolExecutor</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">ENABLE_PARALLEL_OCR</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;ENABLE_PARALLEL_OCR&#34;</span><span class="p">,</span> <span class="s2">&#34;false&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">STRIP_OCR_WORKERS</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;STRIP_OCR_WORKERS&#34;</span><span class="p">,</span> <span class="s2">&#34;8&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">def</span> <span class="nf">process_page_strips</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">strips</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">ENABLE_PARALLEL_OCR</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">return</span> <span class="p">[</span><span class="n">ocr_strip</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">strips</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">with</span> <span class="n">ThreadPoolExecutor</span><span class="p">(</span><span class="n">max_workers</span><span class="o">=</span><span class="n">STRIP_OCR_WORKERS</span><span class="p">)</span> <span class="k">as</span> <span class="n">pool</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">futures</span> <span class="o">=</span> <span class="p">[</span><span class="n">pool</span><span class="o">.</span><span class="n">submit</span><span class="p">(</span><span class="n">ocr_strip</span><span class="p">,</span> <span class="n">page</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">strips</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">return</span> <span class="p">[</span><span class="n">f</span><span class="o">.</span><span class="n">result</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">futures</span><span class="p">]</span></span></span></code></pre></div><h3 id="the-deployment">The deployment</h3>
<p>We didn&rsquo;t flip everything on at once:</p>
<ol>
<li>Deploy with <code>ENABLE_PARALLEL_OCR=false</code> - just the <code>doc.close()</code> fixes</li>
<li>Monitor memory for 24 hours to confirm the leak is fixed</li>
<li>Bump resource limits from <code>4Gi</code> to <code>6Gi</code> to accommodate parallel processing headroom</li>
<li>Enable parallel OCR: <code>ENABLE_PARALLEL_OCR=true</code></li>
</ol>
<p>Memory dropped from a steady 4GB to a stable 1.8GB. That&rsquo;s a 55% reduction from just closing document handles properly.</p>
<h3 id="the-lesson-3">The lesson</h3>
<p><strong>C extension libraries manage their own memory.</strong> Python&rsquo;s GC has no visibility into <code>fitz</code>, <code>numpy</code>, <code>Pillow</code>, or any other C-backed library. If the library provides a <code>close()</code>, use it. If it supports context managers, use <code>with</code>. And always wrap C-heavy processing in <code>try/finally</code>.</p>
<h2 id="the-combined-impact">The combined impact</h2>
<p>Here&rsquo;s the before and after across both services:</p>
<table>
  <thead>
      <tr>
          <th>Issue</th>
          <th>Before</th>
          <th>After</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Unawaited coroutines</td>
          <td>~200MB/hour leak</td>
          <td>0</td>
      </tr>
      <tr>
          <td>MongoClient per call</td>
          <td>5–15 new connections per request</td>
          <td>1 pooled</td>
      </tr>
      <tr>
          <td>Stale cache entries</td>
          <td>Up to 200MB unbounded</td>
          <td>Cleaned every 60s</td>
      </tr>
      <tr>
          <td>PyMuPDF documents</td>
          <td>4GB+ with no release</td>
          <td>Stable at 1.8GB</td>
      </tr>
      <tr>
          <td>Embedding API calls</td>
          <td>2× per document save</td>
          <td>1× (cached concat)</td>
      </tr>
  </tbody>
</table>
<p>Service A went from daily OOMKills to zero restarts. Service B went from crashing every 8–10 hours to running for weeks without a restart.</p>
<h2 id="how-i-found-them">How I found them</h2>
<p>No fancy tooling. Here&rsquo;s the actual process:</p>
<ol>
<li>
<p><strong>Look at the memory graph.</strong> Linear growth = accumulation leak. Sawtooth that doesn&rsquo;t return to baseline = handle/resource leak.</p>
</li>
<li>
<p><strong>Read the code.</strong> Seriously. I read every function in the hot paths and asked &ldquo;what gets allocated here that doesn&rsquo;t get freed?&rdquo;</p>
</li>
<li>
<p><strong><code>grep</code> for patterns.</strong> Once I found one unawaited coroutine, I searched for the pattern across the entire codebase:</p>
</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Find async functions called without await</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">rg <span class="s2">&#34;^\s+[a-z_]+\(&#34;</span> --type py <span class="p">|</span> grep -v <span class="s2">&#34;await &#34;</span> <span class="p">|</span> grep -v <span class="s2">&#34;def &#34;</span> <span class="p">|</span> grep -v <span class="s2">&#34;#&#34;</span></span></span></code></pre></div><p>Not perfect, but it flagged the 13 MongoClient instantiations immediately.</p>
<ol start="4">
<li>
<p><strong>Check warnings.</strong> Enabled <code>RuntimeWarning</code> filtering and got 6 unawaited coroutine warnings within the first minute of startup.</p>
</li>
<li>
<p><strong>Kubernetes events.</strong> <code>kubectl describe pod</code> shows OOMKilled with the exact memory at death. Compare that to your resource limits and you know how fast it&rsquo;s growing.</p>
</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">kubectl get events --field-selector <span class="nv">reason</span><span class="o">=</span>OOMKilling <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">  --sort-by<span class="o">=</span><span class="s1">&#39;.lastTimestamp&#39;</span> -n your-namespace</span></span></code></pre></div><h2 id="a-checklist-for-your-services">A checklist for your services</h2>
<p>If you&rsquo;re running Python services on Kubernetes and seeing memory growth, check these in order:</p>
<ul>
<li><input disabled="" type="checkbox"> <strong>Unawaited coroutines.</strong> Search for <code>RuntimeWarning: coroutine.*was never awaited</code> in your logs. Better yet, make it an error.</li>
<li><input disabled="" type="checkbox"> <strong>MongoClient instantiation.</strong> <code>grep -rn &quot;MongoClient(&quot; your_code/</code> - if it appears in more than one place (especially inside functions, not module level), you probably have a leak.</li>
<li><input disabled="" type="checkbox"> <strong>In-memory caches without eviction.</strong> Any <code>dict</code> that grows with usage and has no <code>del</code> or size cap is a leak. <code>TTLCache</code>, <code>LRU</code>, or <code>maxsize</code> - pick one.</li>
<li><input disabled="" type="checkbox"> <strong>C extension resources.</strong> PyMuPDF, Pillow, lxml, pdfplumber - anything that wraps C needs explicit <code>close()</code> or <code>with</code> statements.</li>
<li><input disabled="" type="checkbox"> <strong>Large objects in exception handlers.</strong> If your <code>except</code> block captures the full traceback and logs the local variables, those locals (which may include entire documents) are pinned until the log entry is flushed.</li>
<li><input disabled="" type="checkbox"> <strong>Background tasks holding request data.</strong> If <code>asyncio.create_task()</code> captures a closure over request objects, those objects live until the task completes.</li>
</ul>
<h2 id="final-thought">Final thought</h2>
<p>None of these bugs produced an error. No stack trace. No warning (well, one did, but it was buried). The services ran <em>fine</em> - they just got slower and slower until Kubernetes killed them, then they restarted and the cycle began again.</p>
<p>Memory leaks in garbage-collected languages are not the &ldquo;forgot to call <code>free()</code>&rdquo; kind. They&rsquo;re the &ldquo;I&rsquo;m holding a reference I didn&rsquo;t know about&rdquo; kind. The GC is doing its job perfectly - it&rsquo;s keeping alive exactly what you told it to.</p>
<p>You just told it wrong.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
