<?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: lucy-reeves</title>
    <description>The latest articles on DEV Community by lucy-reeves (@lucyreeves).</description>
    <link>https://dev.to/lucyreeves</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%2F4178454%2F132cef4e-b445-4cfb-bff0-47b0f45324e3.png</url>
      <title>DEV Community: lucy-reeves</title>
      <link>https://dev.to/lucyreeves</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucyreeves"/>
    <language>en</language>
    <item>
      <title>taming burst traffic: pgbouncer + redis cache for low‑latency queries</title>
      <dc:creator>lucy-reeves</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:02:19 +0000</pubDate>
      <link>https://dev.to/lucyreeves/taming-burst-traffic-pgbouncer-redis-cache-for-low-latency-queries-h9i</link>
      <guid>https://dev.to/lucyreeves/taming-burst-traffic-pgbouncer-redis-cache-for-low-latency-queries-h9i</guid>
      <description>&lt;p&gt;in a SaaS billing service we hit a sudden spike every time a new plan rolled out – the API surged past 10k rps and our PostgreSQL nodes started queuing, pushing response times past 300 ms. the fix landed on a hybrid connection‑pool + cache layer that kept the hot path under 50 ms without over‑provisioning the DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  the bottleneck
&lt;/h2&gt;

&lt;p&gt;when the burst hit, the primary culprit was a flood of identical SELECTs for subscription status. each request opened a fresh TCP socket, negotiated SSL, and then waited for a free backend connection. even with pgbench‑tuned max_connections, the kernel hit its limit and started rejecting new sockets, causing retries in the client library.&lt;/p&gt;

&lt;h2&gt;
  
  
  why pgbouncer?
&lt;/h2&gt;

&lt;p&gt;pgbouncer sits between the app and PostgreSQL, reusing a small pool of persistent connections while multiplexing many client sessions. this reduces TLS handshake overhead and keeps the number of server‑side processes low. the trade‑off is that long‑running transactions can starve others if not timed out.&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=pg-primary port=5432 dbname=mydb pool_size=100 reserve_pool=20&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;listen_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
&lt;span class="py"&gt;listen_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;6432&lt;/span&gt;
&lt;span class="py"&gt;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;md5&lt;/span&gt;
&lt;span class="py"&gt;auth_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/pgbouncer/userlist.txt&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;2000&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;50&lt;/span&gt;
&lt;span class="py"&gt;reserve_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;server_idle_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the &lt;code&gt;reserve_pool&lt;/code&gt; lets a few extra sessions bypass the main pool when the main one is saturated, preventing a hard wall at &lt;code&gt;max_client_conn&lt;/code&gt;. however, setting it too high can let a burst swamp the DB again, so we couple it with a low &lt;code&gt;server_idle_timeout&lt;/code&gt; to recycle idle connections quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  adding a redis read‑through cache
&lt;/h2&gt;

&lt;p&gt;for the hot SELECT we introduced a Redis read‑through layer. the first request hits PostgreSQL, stores the result with a TTL, and subsequent requests pull from Redis. this cuts the DB load dramatically, but introduces cache‑staleness risk. we mitigated it by invalidating the key on any UPDATE/INSERT to the subscription table using a simple NOTIFY/LISTEN trigger.&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_subscription_change&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'subscription_change'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TG_TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="n"&gt;sub_change&lt;/span&gt; &lt;span class="k"&gt;AFTER&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;subscriptions&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;EACH&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_subscription_change&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the listener in the app clears the Redis entry, forcing a fresh DB read on the next request. the edge case to watch: if the listener crashes, stale data can persist until TTL expires, so we also set a max‑age fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  trade‑offs and failure modes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pool sizing&lt;/strong&gt; – too small and you get queue latency; too large and you risk exhausting DB resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cache consistency&lt;/strong&gt; – eventual consistency is acceptable for billing reads, but not for write‑through paths; always pair invalidation with a fallback DB fetch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;network partitions&lt;/strong&gt; – if Redis becomes unavailable, the app should gracefully fall back to DB reads; circuit‑breaker patterns help here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  results and next steps
&lt;/h2&gt;

&lt;p&gt;after deploying pgBouncer with a 50‑connection pool and a 5‑second Redis TTL, the 10k rps burst settled to ~45 ms avg latency, and PostgreSQL CPU dropped from 85 % to 30 %. the system now handles future plan roll‑outs with headroom.&lt;/p&gt;

&lt;p&gt;what patterns have you used to keep query latency low under bursty traffic, and how do you balance pool size against cache consistency?&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>redis</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>why pg‑bouncer can still choke your service under burst load</title>
      <dc:creator>lucy-reeves</dc:creator>
      <pubDate>Sun, 18 Oct 2026 12:28:17 +0000</pubDate>
      <link>https://dev.to/lucyreeves/why-pg-bouncer-can-still-choke-your-service-under-burst-load-2ogn</link>
      <guid>https://dev.to/lucyreeves/why-pg-bouncer-can-still-choke-your-service-under-burst-load-2ogn</guid>
      <description>&lt;h2&gt;
  
  
  problem statement
&lt;/h2&gt;

&lt;p&gt;in a typical microservice, each instance opens its own pg client pool. during a traffic spike the aggregate connection count can exceed the postgres &lt;code&gt;max_connections&lt;/code&gt; limit, leading to &lt;code&gt;FATAL: remaining connection slots are reserved for non-replication superuser&lt;/code&gt; errors. the usual fix is to front the database with pg‑bouncer, but many teams still see latency spikes and dropped requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  why a naive pool still hurts
&lt;/h2&gt;

&lt;p&gt;pg‑bouncer operates in transaction pooling mode by default. it hands out a physical backend connection only for the duration of a transaction, then returns it to the pool. if your service opens a transaction, performs a long async task, then commits, the connection sits idle while the task runs. bursts of such patterns quickly exhaust the pool, and pg‑bouncer starts queuing new client connections, inflating response times.&lt;/p&gt;

&lt;h2&gt;
  
  
  a tighter configuration
&lt;/h2&gt;

&lt;p&gt;switching to &lt;em&gt;statement&lt;/em&gt; pooling can mitigate the idle‑time problem, but it introduces its own edge cases: session‑level settings (e.g., &lt;code&gt;SET timezone&lt;/code&gt;) and temporary tables are lost between statements. a pragmatic compromise is to keep transaction pooling but enforce a strict client‑side timeout and keep transaction bodies minimal.&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="c"&gt;# pgbouncer.ini
&lt;/span&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=10.1.2.3 port=5432 dbname=mydb pool_mode=transaction&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&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          # total connections from services&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          # per‑database backend connections&lt;/span&gt;
&lt;span class="py"&gt;reserve_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5           # extra connections for spikes&lt;/span&gt;
&lt;span class="py"&gt;reserve_pool_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5        # seconds before using reserve pool&lt;/span&gt;
&lt;span class="py"&gt;server_idle_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;300       # close idle backends after 5 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the &lt;code&gt;reserve_pool_*&lt;/code&gt; settings give a small buffer for sudden bursts without permanently allocating more backends. combine this with a service‑level circuit breaker that backs off when the pool queue length exceeds a threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  trade‑offs to watch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;statement vs transaction pooling&lt;/strong&gt; – statement mode removes idle time but breaks any session‑state reliance; audit your code for &lt;code&gt;SET&lt;/code&gt; commands or temp tables before switching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reserve pool size&lt;/strong&gt; – setting it too high can starve other databases on the same postgres instance; too low and spikes still cause queuing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;client timeout&lt;/strong&gt; – aggressive timeouts prevent long‑running transactions from hogging connections, but may surface hidden latency bugs in upstream services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;monitoring&lt;/strong&gt; – expose &lt;code&gt;SHOW POOLS&lt;/code&gt; from pg‑bouncer and track &lt;code&gt;total_wait&lt;/code&gt; and &lt;code&gt;maxwait_us&lt;/code&gt;; a sudden rise signals that your reserve pool is being consumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  production lesson
&lt;/h2&gt;

&lt;p&gt;in our SaaS, moving from the default &lt;code&gt;max_client_conn=200&lt;/code&gt; to a tiered reserve pool cut 95th‑percentile latency from 800 ms to 250 ms during flash‑sale traffic, without increasing the postgres &lt;code&gt;max_connections&lt;/code&gt;. the key was tightening client‑side transaction boundaries and adding a lightweight back‑off in the HTTP layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  closing thought
&lt;/h2&gt;

&lt;p&gt;how do you balance pg‑bouncer pooling mode, reserve pool sizing, and service‑side timeouts in a high‑concurrency stack&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backend</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Why Your Connection Pool is Lying to You</title>
      <dc:creator>lucy-reeves</dc:creator>
      <pubDate>Thu, 15 Oct 2026 03:24:06 +0000</pubDate>
      <link>https://dev.to/lucyreeves/why-your-connection-pool-is-lying-to-you-4pel</link>
      <guid>https://dev.to/lucyreeves/why-your-connection-pool-is-lying-to-you-4pel</guid>
      <description>&lt;p&gt;We hit a wall last month. Traffic was stable, CPU was idle, but p99 latency spiked to 2s. The PostgreSQL server looked healthy. The culprit wasn't the database; it was our Node.js connection pool configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Illusion of Saturation
&lt;/h2&gt;

&lt;p&gt;Most teams set &lt;code&gt;pool.max&lt;/code&gt; based on a static assumption: "We have 20 connections, so we can handle 20 concurrent requests." This fails because connections aren't just resources; they are locks. When you hold a connection for 50ms, you block 19 others. When you hold it for 500ms due to a slow downstream call or a complex join, you starve the queue.&lt;/p&gt;

&lt;p&gt;The standard &lt;code&gt;pg&lt;/code&gt; pool in Node behaves like a fixed-size barrier. If all &lt;code&gt;max&lt;/code&gt; connections are in use, new queries wait in a FIFO queue. There is no fallback. There is no read replica offload. Just waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottleneck
&lt;/h2&gt;

&lt;p&gt;Our logs showed &lt;code&gt;queryQueueLength&lt;/code&gt; climbing steadily. We weren't seeing duplicate queries, but we were seeing long-held connections. Specifically, we had a service that called an external API inside a transaction. The transaction had &lt;code&gt;idleInTransactionSessionTimeout&lt;/code&gt; set to 60s. The API was timing out at 5s, but the connection stayed open until the timeout or manual commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The mistake: holding a connection during external I/O&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&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;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://external-api.com/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Blocks for 1-5s&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE users SET last_synced = NOW() WHERE id = $1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Too late, the queue was already backed up&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix wasn't scaling out the postgres instance. It was architectural separation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoff
&lt;/h2&gt;

&lt;p&gt;We split the lifecycle. We run the external API call &lt;em&gt;outside&lt;/em&gt; the database transaction. Yes, this introduces a consistency window. If the API fails after we commit the intent, we have stale data. But in our case, eventual consistency was acceptable for sync status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Better: Short-lived transaction, external I/O separated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN; UPDATE sync_state SET status = $1 WHERE id = $2; COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&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;finalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&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;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://external-api.com/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;finalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;finalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BEGIN; UPDATE sync_state SET status = $1 WHERE id = $2; COMMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;finalStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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 p99 dropped to 80ms. The queue depth flattened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Cases to Watch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nested Transactions&lt;/strong&gt;: If your ORM supports savepoints, be careful. &lt;code&gt;SAVEPOINT&lt;/code&gt; doesn't release the underlying connection, but it adds overhead. Avoid them in tight loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statement Caching&lt;/strong&gt;: In Postgres 13+, prepared statements are cached per connection. If you have high-churn connection recycling, you might miss the cache benefits. &lt;code&gt;node-postgres&lt;/code&gt; handles this, but custom drivers might not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Don't just watch &lt;code&gt;active_connections&lt;/code&gt;. Watch &lt;code&gt;backend_xact_start&lt;/code&gt; age. Long-running transactions are the real killers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Connection pooling is not a silver bullet; it's a buffer against mismanagement. If your code holds DB connections while talking to other services, you're not pooling, you're hoarding.&lt;/p&gt;

&lt;p&gt;How do you handle external I/O dependencies that logically require a DB transaction context? Do you use saga patterns, or do you rely on idempotency and eventual consistency?&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
