<?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>useEffect Fired Twice and It Found a Real Bug</title>
      <dc:creator>Rohit Bhadani</dc:creator>
      <pubDate>Sat, 05 Sep 2026 03:34:36 +0000</pubDate>
      <link>https://dev.to/rbonweb/useeffect-fired-twice-and-it-found-a-real-bug-5438</link>
      <guid>https://dev.to/rbonweb/useeffect-fired-twice-and-it-found-a-real-bug-5438</guid>
      <description>&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; fired twice, on mount, every single time, in development only. The API call inside it — a POST that created a resource — ran twice, and for about a day we had duplicate records showing up in a table that should have had exactly one insert per page load.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first reaction, and why it was wrong
&lt;/h2&gt;

&lt;p&gt;The instinct is to assume a bug — a rerender loop, a missing dependency, something actually broken. React 18's Strict Mode, in development, deliberately mounts, unmounts, and remounts every component once, specifically to surface effects that aren't properly cleaned up. It's not a bug in your code causing a double-fire; it's a bug in your code being caught by a feature built to catch exactly this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// logs twice in dev, once in production&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subscribeToUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// no cleanup — this is the actual problem Strict Mode is surfacing&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Production builds don't do this double-invocation — it's development-only, and specifically Strict-Mode-only, which is why the duplicate inserts we saw locally would eventually have shown up in production too, just less predictably, under a race condition instead of a guaranteed double-fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a feature and not noise to suppress
&lt;/h2&gt;

&lt;p&gt;An effect that safely tolerates being mounted, torn down, and mounted again is an effect that correctly declares its dependencies and cleans up after itself — which is exactly the property you need for effects to behave correctly under React's concurrent features generally, not just under Strict Mode specifically. The double-invocation in development is a cheap, automatic test for that property, running on every single page load without you writing a test for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;subscribeToUpdates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// cleanup makes remounting safe&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our specific case — a POST that shouldn't fire twice regardless of mount behavior — the deeper fix was recognizing that a side effect with real-world consequences (creating a database row) needs to be idempotent or explicitly guarded, because "this only runs once" was never a guarantee &lt;code&gt;useEffect&lt;/code&gt; actually made, even before Strict Mode started enforcing the point loudly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createResource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cancelled&lt;/code&gt; flag doesn't stop the double mount from happening — it stops the second mount's effect from acting on a component that's already been torn down, which is the actual property that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we caught this before it reached more customers
&lt;/h2&gt;

&lt;p&gt;The double-insert bug was already live before Strict Mode's double-fire made us go looking for it, which is the more important point: local development would have looked completely fine without Strict Mode enabled, and the race condition in production might have taken far longer to notice, showing up as an occasional support ticket about duplicate items rather than an obvious, reproducible pattern. We keep Strict Mode on for every environment we can, including staging deployments on disposable &lt;a href="https://krova.cloud/?ref=devto-krova" rel="noopener noreferrer"&gt;Krova&lt;/a&gt; Cubes that mirror production traffic patterns, specifically because catching this class of bug before a customer does is worth the minor noise of double-logged console output. I run Krova, so take the staging setup as informed rather than neutral, but enabling Strict Mode everywhere you reasonably can is free advice regardless of host.&lt;/p&gt;

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

&lt;p&gt;If an effect behaves differently than expected only in development, check whether you're running React 18+ with Strict Mode before assuming a logic bug. Then check whether the effect has a cleanup function and whether it would still behave correctly if mounted, unmounted, and mounted again in rapid succession — because that's the actual property Strict Mode is testing, and it's a property your effect needs regardless of whether Strict Mode is the thing that exposed its absence.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The CORS Header Was Right There and the Browser Blocked It Anyway</title>
      <dc:creator>Rohit Bhadani</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:44:26 +0000</pubDate>
      <link>https://dev.to/rbonweb/the-cors-header-was-right-there-and-the-browser-blocked-it-anyway-dgo</link>
      <guid>https://dev.to/rbonweb/the-cors-header-was-right-there-and-the-browser-blocked-it-anyway-dgo</guid>
      <description>&lt;p&gt;The browser console showed exactly what CORS errors always show — a request blocked for violating the same-origin policy — except the response headers, visible in the network tab, clearly included &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;. The header the browser wanted was right there. The browser rejected the request anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that's easy to miss in the network tab
&lt;/h2&gt;

&lt;p&gt;Chrome's network inspector, by default, coalesces duplicate header names into a single display line — so &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; shown once in the UI can actually mean the header was sent &lt;strong&gt;twice&lt;/strong&gt; by the server, and the browser is showing you a merged, deduplicated view rather than the literal wire response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - https://api.example.com/data &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; access-control
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://app.example.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate headers, both valid individually, sent by two different layers that each thought they were the one responsible for CORS: our nginx reverse proxy had a blanket &lt;code&gt;add_header Access-Control-Allow-Origin *;&lt;/code&gt; for general API access, and the application server behind it independently set a specific origin for authenticated routes. Neither config was wrong on its own. Together, they produced a response with the header appearing twice — and per the Fetch spec, a response with multiple &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; values is treated as invalid, so the browser blocks the request rather than guessing which one you meant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worse than a missing header
&lt;/h2&gt;

&lt;p&gt;A missing CORS header fails immediately, obviously, the same way every time. A duplicate header fails in a way that looks, from the response body alone, like the header is present and correct — because it is present, twice, which is precisely the state that trips the spec's validation. Every piece of evidence you'd normally check says "this should work," and it still doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Removed the blanket nginx header and let the application server be the single source of truth for CORS decisions, since it's the layer that actually knows which origins should be allowed for which routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# removed this — the app now owns CORS entirely&lt;/span&gt;
&lt;span class="c1"&gt;# add_header Access-Control-Allow-Origin *;&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://app_upstream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;# no CORS headers added here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One layer, one header, no duplication possible by construction rather than by discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this kind of overlap tends to hide
&lt;/h2&gt;

&lt;p&gt;This class of bug — two layers of infrastructure each independently and correctly configured, producing an incorrect result only in combination — is exactly why we keep our ingress configuration minimal on &lt;a href="https://krova.cloud/?ref=devto-krova" rel="noopener noreferrer"&gt;Krova&lt;/a&gt;: the managed HTTPS ingress terminates TLS and forwards to your Cube without injecting application-level headers like CORS on your behalf, so there's exactly one place — your own app or reverse proxy — that owns those decisions, not two competing ones split across a platform layer and your own config without either side being aware of the other. I run Krova, so take the specific setup as informed rather than neutral, but the debugging lesson generalizes: when a header looks right in the browser but the request still fails, check for duplicates on the wire, not just presence.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;curl -sD -&lt;/code&gt; against the failing endpoint, piped through &lt;code&gt;grep -i access-control&lt;/code&gt;, before trusting anything the browser's network tab shows you. If you see the same header name more than once, that's the entire bug — find both places setting it and remove all but one.&lt;/p&gt;

</description>
      <category>cors</category>
      <category>webdev</category>
      <category>nginx</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Ran git reset --hard in the Wrong Window</title>
      <dc:creator>Rohit Bhadani</dc:creator>
      <pubDate>Thu, 03 Sep 2026 03:38:02 +0000</pubDate>
      <link>https://dev.to/rbonweb/i-ran-git-reset-hard-in-the-wrong-window-2ihd</link>
      <guid>https://dev.to/rbonweb/i-ran-git-reset-hard-in-the-wrong-window-2ihd</guid>
      <description>&lt;p&gt;&lt;code&gt;git reset --hard HEAD~3&lt;/code&gt; — run in the wrong repository window, at 6:40pm, immediately followed by the specific kind of silence that happens when you realize what you just did before your brain finishes processing it. Three commits of uncommitted-adjacent work, gone from the working tree in under a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first, most important fact: it's very likely still there
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git reset --hard&lt;/code&gt; moves the branch pointer and resets the working tree, but Git doesn't actually delete commit objects just because nothing points at them anymore — they sit in the object database, unreferenced, until garbage collection eventually cleans them up, which for most repos happens rarely enough that "eventually" can mean weeks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;a1b2c3d HEAD@{0}: reset: moving to HEAD~3
e4f5g6h HEAD@{1}: commit: add retry logic to payment webhook
7h8i9j0 HEAD@{2}: commit: fix currency rounding
k1l2m3n HEAD@{3}: commit: initial webhook handler
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reflog is a local log of everywhere &lt;code&gt;HEAD&lt;/code&gt; has pointed recently, and it survives a reset because a reset is just another entry in it, not an erasure of the ones before it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; e4f5g6h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working tree restored to exactly the state before the reset, all three commits back, in the time it takes to read this sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the reflog isn't enough
&lt;/h2&gt;

&lt;p&gt;If the commits were never made at all — you ran &lt;code&gt;reset --hard&lt;/code&gt; on genuinely uncommitted changes — the reflog can't help, because it only tracks where &lt;code&gt;HEAD&lt;/code&gt; and branches have pointed, not file contents that were never committed. That's a real loss, and the only real defense against it is committing early and often, including throwaway "wip" commits you intend to squash later, specifically because an uncommitted change has no recovery path at all.&lt;/p&gt;

&lt;p&gt;If the commits were committed and the reflog entry has expired — Git's default is to keep unreachable reflog entries for 90 days, reachable ones for longer — &lt;code&gt;git fsck --unreachable&lt;/code&gt; can sometimes still find dangling commit objects directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fsck &lt;span class="nt"&gt;--unreachable&lt;/span&gt; &lt;span class="nt"&gt;--no-reflog&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;commit
git show &amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="c"&gt;# inspect before deciding it's the one you want&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I test destructive Git operations before running them for real, now
&lt;/h2&gt;

&lt;p&gt;The reflog saved this specific mistake, and it also taught me not to rely on remembering it exists at 6:40pm under stress. The actual habit that stuck: for anything genuinely destructive and unfamiliar — an interactive rebase across a long history, a filter-branch, anything with &lt;code&gt;--hard&lt;/code&gt; or &lt;code&gt;--force&lt;/code&gt; in it — I run it first against a disposable clone of the repo, confirm it does what I think it does, then run it for real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# rehearse the risky operation somewhere it can't hurt anything&lt;/span&gt;
krova cubes create git-rehearsal &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="nt"&gt;--ram&lt;/span&gt; 2 &lt;span class="nt"&gt;--disk&lt;/span&gt; 10
&lt;span class="c"&gt;# clone, attempt the operation, inspect the result, then destroy regardless of outcome&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the rehearsal on a disposable &lt;a href="https://krova.cloud/?ref=devto-krova" rel="noopener noreferrer"&gt;Krova&lt;/a&gt; Cube costs a few minutes and effectively nothing, and destroying it afterward means there's no cleanup step to skip — the environment simply stops existing once I have my answer. I run Krova, so read the specific workflow as informed rather than neutral, but the underlying discipline — rehearse destructive Git operations somewhere disposable before running them on a repo you care about — is worth adopting regardless of what you rehearse it on.&lt;/p&gt;

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

&lt;p&gt;Before assuming lost work is actually lost, run &lt;code&gt;git reflog&lt;/code&gt; and read it fully — the commit you want is very likely sitting in there under a hash you haven't looked at yet. And before your next genuinely destructive Git command, consider whether rehearsing it somewhere disposable costs you anything at all compared to finding out the hard way.&lt;/p&gt;

</description>
      <category>git</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <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>
