<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[RagasImger]]></title><description><![CDATA[Whoops!]]></description><link>https://blog.sagarregmi.info.np</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 22:18:49 GMT</lastBuildDate><atom:link href="https://blog.sagarregmi.info.np/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Transaction Pooling: The Multi-Tenant Nightmare]]></title><description><![CDATA[This critique of Transaction Pooling in PgBouncer targets multi-tenant PostgreSQL setups that rely on session-level state (e.g., schema-per-tenant using search_path). Because Transaction Pooling discards session state after each transaction, it cause...]]></description><link>https://blog.sagarregmi.info.np/transaction-pooling-the-multi-tenant-nightmare</link><guid isPermaLink="true">https://blog.sagarregmi.info.np/transaction-pooling-the-multi-tenant-nightmare</guid><category><![CDATA[PgBouncer]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Databases]]></category><category><![CDATA[#multitenancy]]></category><category><![CDATA[multitenant]]></category><dc:creator><![CDATA[Ragas Imger]]></dc:creator><pubDate>Sun, 08 Jun 2025 18:17:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749405285801/03bc08c1-ce0c-4a40-b514-3561cbd000b0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This critique of Transaction Pooling in PgBouncer targets multi-tenant PostgreSQL setups that rely on session-level state (e.g., schema-per-tenant using <code>search_path</code>). Because Transaction Pooling discards session state after each transaction, it causes serious cross-tenant data leaks and confusion in these cases. If your app is single-tenant or fully stateless (not relying on session state), Transaction Pooling may work fine. Always evaluate your app’s architecture before enabling Transaction Pooling.</p>
<p>This blog focuses <strong>specifically on multi-tenancy architectures</strong> — especially schema-per-tenant setups — in PostgreSQL. If you're not building a multi-tenant app, some of these warnings may not apply to you (but stick around anyway — you might still enjoy the tea ☕)</p>
</blockquote>
<h2 id="heading-why-using-transaction-pooling-in-pgbouncer-with-multi-tenancy-is-a-disaster-waiting-to-happen"><strong>Why Using Transaction Pooling in PgBouncer with Multi-Tenancy Is a Disaster Waiting to Happen?</strong></h2>
<p>Let’s not pretend this is fine. You’ve built a beautiful multi-tenant SaaS app. You’re optimizing performance. You install PgBouncer, flip it into <strong>Transaction Pooling mode</strong>, and feel proud — but then, surprise! Tenant A is suddenly seeing Tenant B’s data.</p>
<p>Welcome to the wild world of <strong>connection pooling gone wrong</strong>. If you’re using PgBouncer with <strong>multi-tenant PostgreSQL</strong>, and you think <strong>Transaction</strong> or worse — <strong>Statement Pooling</strong> is okay, sit down. We need to talk. This is your <strong>intervention</strong>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>You’re not scaling. You’re leaking.</strong></div>
</div>

<h2 id="heading-pgbouncer-pooling-modes-101"><strong>PgBouncer Pooling Modes 101</strong></h2>
<blockquote>
<p>(AKA the “how not to shoot your foot” guide)</p>
</blockquote>
<p>PgBouncer is amazing — it lets you have thousands of app connections without overwhelming PostgreSQL. It offers 3 pooling modes:</p>
<ol>
<li><p><strong>Session Pooling</strong> — Each client gets their own server connection until they disconnect. Like renting a cabin in the woods. Safe. Boring. Effective.</p>
</li>
<li><p><strong>Transaction Pooling</strong> — You return the connection after each transaction. Like Airbnb’ing your room out to strangers every night while you're still living in it.</p>
</li>
<li><p><strong>Statement Pooling</strong> — Every SQL statement uses a fresh connection. It’s like passing your toothbrush around after every use. Just don’t.</p>
</li>
</ol>
<p>‘‘<em>If you just said “ew,” congrats — you already understand why this blog exists</em>.’’</p>
<h2 id="heading-what-even-is-multi-tenancy"><strong>What Even <em>Is</em> Multi-Tenancy?</strong></h2>
<p>Multi-tenancy means one app serves multiple clients (a.k.a tenants) but pretends like each has their own private mansion. In PostgreSQL, the common strategies are:</p>
<ul>
<li><p><strong>Schema-per-tenant:</strong> Every tenant gets their own schema. Classy, organized, scalable.</p>
</li>
<li><p><strong>Row-level isolation:</strong> All tenants live in the same tables, separated by tenant_id. Like co-working space — cheaper, but messier.</p>
</li>
<li><p><strong>Database-per-tenant:</strong> The gated community of multi-tenancy (not our focus here).</p>
</li>
</ul>
<p>We’ll focus on <strong>schema-per-tenant</strong>, where the app dynamically sets the <code>search_path</code> on every request to ensure PostgreSQL targets the correct schema for that tenant.</p>
<p>Sounds fancy. Until you accidentally serve Ram’s data to Hari. Then it sounds like a lawsuit.</p>
<h2 id="heading-why-transaction-pooling-is-your-apps-worst-enemy"><strong>Why Transaction Pooling Is Your App’s Worst Enemy?</strong></h2>
<ol>
<li><p><strong>Connection State? What State?:</strong>  </p>
<p> In <strong>Transaction Pooling</strong>, PgBouncer gives your app a fresh connection for each transaction — but it doesn’t clean up session-level settings like <code>search_path</code>. So if you don’t manually reset them, <strong>they might leak</strong> into the next request.<br /> In <strong>Statement Pooling</strong>, it’s even harsher — PgBouncer throws away everything after every SQL statement. That means session-level config just vanishes.  </p>
<p> <em>Imagine renting a hotel room and finding the last guest’s toothbrush still wet. That’s your app in Transaction Pooling mode.</em><br /> <em>In Statement Pooling? You get a brand-new room and toothbrush every time — but you can’t leave anything behind, not even your towel.</em>  </p>
</li>
<li><p><strong>When Connections Play Musical Chairs (And Your Data Gets Caught in the Game):</strong><br /> Let’s be honest:</p>
<ol>
<li><p>Thread A sets search_path = tenant_a, finishes the transaction</p>
</li>
<li><p>Thread B reuses the connection, assumes it’s for tenant_b, but still sees tenant_a's context</p>
</li>
<li><p>Result: <strong>tenant_b sees tenant_a’s data</strong>.</p>
</li>
</ol>
</li>
</ol>
<p>    You won’t spot this in dev or UAT. It lurks in production, until your CEO gets an angry email:  </p>
<p>    <strong>“Why is another company’s invoice in my dashboard?”  
    </strong>Congratulations. You just summoned the final boss: <em>The Data Protection Authority</em>.  </p>
<ol start="3">
<li><p><strong>Session-Level Features? Forget About It</strong><br /> Session-level magic like:</p>
<ol>
<li><p>SET search_path</p>
</li>
<li><p>Temporary tables</p>
</li>
<li><p>Tenant-specific GUCs (like <code>preferred_language = 'np'</code>)</p>
</li>
<li><p>Advisory locks.</p>
</li>
</ol>
</li>
</ol>
<p>    ...all break or <strong>leak</strong> under aggressive pooling.<br />    You paid for that tea, but some sneaky guy just swiped your cup and disappeared like it was free <strong><em>chai</em></strong> <em>☕.</em>  </p>
<ol start="4">
<li><p><strong>The Code Becomes a Messy Crime Scene</strong><br /> To avoid disaster with Transaction Pooling, you'd have to:</p>
<ol>
<li><p>Reset search_path on <strong>every</strong> request</p>
</li>
<li><p>Clean up all lingering session state</p>
</li>
<li><p>Pray your ORM doesn’t cache anything nasty.</p>
</li>
</ol>
</li>
</ol>
<p>Your clean architecture? Now it’s a patchwork of hacks and prayers your production won’t explode.</p>
<blockquote>
<p>Let’s get fancy with the heading — it deserves it.</p>
</blockquote>
<h2 id="heading-when-is-transaction-pooling-actually-okay"><strong>😇 When Is Transaction Pooling Actually Okay?</strong></h2>
<p>Almost never. But <em>if</em>:</p>
<ul>
<li><p>Your app is 100% stateless (no schema switching, no temp tables)</p>
</li>
<li><p>You hate complexity more than you love safety</p>
</li>
<li><p>You enjoy living on the edge (querying prod on a Friday night).</p>
</li>
</ul>
<p>Then maybe — <em>maybe</em> — it’s okay.</p>
<p>But remember: just because it works on your machine doesn’t mean it won’t destroy your prod.</p>
<h2 id="heading-do-this-instead-use-session-pooling"><strong>🛡️ Do This Instead: Use Session Pooling</strong></h2>
<p>Session Pooling may not be flashy, but it’s reliable — it keeps your app <strong>safe</strong>, <strong>predictable</strong>, and <strong>easy-going</strong>.</p>
<p>Why it works:</p>
<ul>
<li><p>Keeps tenant-specific search_path intact</p>
</li>
<li><p>Preserves session-level settings</p>
</li>
<li><p>Avoids accidental data leaks.</p>
</li>
</ul>
<p>Yes, it uses more memory. Yes, it’s less efficient. But so is wearing a seatbelt. Don’t be the dev that flies through the windshield.</p>
<h2 id="heading-want-performance-without-the-risk"><strong>🧠 Want Performance Without the Risk?</strong></h2>
<p>Stop trying to make Transaction Pooling happen. It's not going to happen. Instead:</p>
<ol>
<li><p><strong>Increase PostgreSQL’s max_connections</strong></p>
</li>
<li><p>Use <strong>separate PgBouncer pools per tenant</strong> (hardcore but effective)</p>
</li>
<li><p>Explore <strong>multiplexing(like</strong> <code>PgCat</code><strong>)</strong> or <strong>app-level routing</strong> to manage tenant-based connection pools.</p>
</li>
</ol>
<p>You want speed, sure. But not at the cost of serving Communist’s secrets to Raa.Swo.Paa.</p>
<h2 id="heading-quick-recap-pooling-modes-amp-multi-tenancy-safety"><strong>Quick Recap: Pooling Modes &amp; Multi-Tenancy Safety</strong></h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td><strong>Transaction/Statement Pooling</strong></td><td><strong>Session Pooling</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Reuses connections fast</td><td>✅</td><td>❌</td></tr>
<tr>
<td>Keeps session state</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Safe for multi-tenancy</td><td>❌</td><td>✅</td></tr>
<tr>
<td>Data leakage risk</td><td><strong>High</strong></td><td>Low</td></tr>
<tr>
<td>Makes you work on weekends</td><td><strong>Often</strong></td><td>Very Rarely</td></tr>
</tbody>
</table>
</div><h2 id="heading-real-world-analogy-the-hotel-key-disaster"><strong>Real-World Analogy: The Hotel Key Disaster</strong></h2>
<p>Imagine a hotel:</p>
<ul>
<li><p>In <strong>Session Pooling</strong>, each guest gets their own key. They enter their room, stay the night, then leave.</p>
</li>
<li><p>In <strong>Transaction Pooling</strong>, every time a guest goes to the bathroom, they hand the key back to the hotel staff. Next guest uses it — and finds someone else’s underwear on the bed.</p>
</li>
</ul>
<p>Don’t build that hotel. Don’t be that dev.</p>
<h2 id="heading-final-thoughts"><strong>Final Thoughts</strong></h2>
<p>If you're building a <strong>multi-tenant app</strong>, <strong>Session Pooling</strong> is the only sane way to use PgBouncer. <strong>Transaction</strong> or <strong>Statement Pooling</strong> may look fast and shiny, but they’re quietly waiting to wreck your tenant isolation and your weekend.</p>
<p>So next time you're tempted to switch modes in pgbouncer.ini, think twice — pick wisely.</p>
]]></content:encoded></item></channel></rss>