<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rohit Bhadani</title>
    <description>The latest articles on DEV Community by Rohit Bhadani (@rbonweb).</description>
    <link>https://dev.to/rbonweb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1229774%2F7a7628f8-e4f6-44d3-b60d-5136a30c4580.jpeg</url>
      <title>DEV Community: Rohit Bhadani</title>
      <link>https://dev.to/rbonweb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rbonweb"/>
    <language>en</language>
    <item>
      <title>Postgres Said Too Many Clients While Sitting at 40% CPU</title>
      <dc:creator>Rohit Bhadani</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:46:03 +0000</pubDate>
      <link>https://dev.to/rbonweb/postgres-said-too-many-clients-while-sitting-at-40-cpu-2if9</link>
      <guid>https://dev.to/rbonweb/postgres-said-too-many-clients-while-sitting-at-40-cpu-2if9</guid>
      <description>&lt;p&gt;The error was &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt;, thrown by a Postgres instance that, by every dashboard we had, was using 40% of its CPU and half its RAM. Plenty of headroom. It rejected the connection anyway, which is the specific kind of outage that makes people distrust their own monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number nobody had looked at
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;max_connections&lt;/code&gt; was set to 100, the Postgres default nobody changes unless something forces the question. We weren't near it on any given request — we were near it in aggregate, because every one of our six app instances kept its own connection pool of 20, and 6 × 20 is 120 before Postgres even finishes its own reserved slots for replication and superuser access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 97, climbing toward 100 during traffic spikes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each connection, busy or idle, holds a backend process in Postgres with its own memory overhead — a few megabytes each, which is why "just raise max_connections to 1000" is the advice that fixes the symptom and quietly creates a memory problem three weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixed it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PgBouncer in transaction mode&lt;/strong&gt;, sitting between the app and Postgres, multiplexing hundreds of app-side connections onto a much smaller pool of real backend connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;mydb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=5432 dbname=mydb&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;pool_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;transaction&lt;/span&gt;
&lt;span class="py"&gt;max_client_conn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1000&lt;/span&gt;
&lt;span class="py"&gt;default_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App-side, nothing changed except the port it connected to. Real Postgres connections dropped from 97 peak to a steady 20, with headroom that didn't depend on how many app instances we happened to be running that week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this stayed invisible so long
&lt;/h2&gt;

&lt;p&gt;The connection ceiling scales with &lt;code&gt;instance count × pool size&lt;/code&gt;, and both of those numbers grow independently, usually for unrelated reasons — you scale instances for traffic, you scale pool size because someone hit a slow-query timeout once and bumped it. Nobody multiplies the two together until Postgres does it for you, out loud, during a traffic spike.&lt;/p&gt;

&lt;p&gt;This is also where the machine underneath quietly matters. We'd been running Postgres on a shared-tenancy VPS where "half your RAM" was a number we trusted less than we should have — on an oversold host, headroom on a dashboard isn't the same guarantee it looks like. Since moving that database onto a &lt;a href="https://krova.cloud/?ref=devto-krova" rel="noopener noreferrer"&gt;Krova&lt;/a&gt; Cube, RAM is reserved 1:1 — no overselling, no thin provisioning — so when &lt;code&gt;pg_stat_activity&lt;/code&gt; and &lt;code&gt;free -h&lt;/code&gt; say there's room, there actually is room, and pooling fixes are fixes rather than guesses against an unknown neighbor's usage. I run Krova, so take the specific plug as informed rather than neutral, but the pooling fix works regardless of host.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd check first
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SELECT count(*) FROM pg_stat_activity;&lt;/code&gt; against &lt;code&gt;SHOW max_connections;&lt;/code&gt;, and separately, &lt;code&gt;(number of app instances) × (pool size per instance)&lt;/code&gt;. If the second number is anywhere near the first, you don't have a Postgres problem, you have an arithmetic problem that Postgres is enforcing on your behalf.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
