<?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: Akshay</title>
    <description>The latest articles on DEV Community by Akshay (@akshay272727).</description>
    <link>https://dev.to/akshay272727</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%2F4028140%2Fa7bde1a3-bfd7-4800-b51a-cb4905223c43.jpg</url>
      <title>DEV Community: Akshay</title>
      <link>https://dev.to/akshay272727</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshay272727"/>
    <language>en</language>
    <item>
      <title>PostgreSQL Connection Strings Explained (DATABASE_URL)</title>
      <dc:creator>Akshay</dc:creator>
      <pubDate>Tue, 14 Jul 2026 18:38:35 +0000</pubDate>
      <link>https://dev.to/akshay272727/postgresql-connection-strings-explained-databaseurl-5cbh</link>
      <guid>https://dev.to/akshay272727/postgresql-connection-strings-explained-databaseurl-5cbh</guid>
      <description>&lt;p&gt;A &lt;strong&gt;PostgreSQL connection string&lt;/strong&gt; is the single line that tells a driver everything it needs to reach your database: which user, which host and port, which database, and how to secure the connection. You usually meet it as the &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable, and every backend tutorial eventually says 'set your &lt;code&gt;DATABASE_URL&lt;/code&gt;' as though its format were self-evident. It nearly is - once someone actually walks you through it. This is that walkthrough: the anatomy of the string, the parameters worth knowing, where it should live, and the four errors everyone hits exactly once.&lt;/p&gt;

&lt;p&gt;![Every segment of a connection string: scheme, user, password, host, port, database, and parameters.]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyl8tenhkyhnli5fkcn9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhyl8tenhkyhnli5fkcn9.png" alt=" " width="799" height="388"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every segment of a connection string: scheme, user, password, host, port, database, and parameters.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a PostgreSQL connection string
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;postgresql://app_user:s3cret@db.example.com:5432/myapp_db?sslmode&lt;span class="o"&gt;=&lt;/span&gt;require
&lt;span class="se"&gt;\_&lt;/span&gt;_______/  &lt;span class="se"&gt;\_&lt;/span&gt;_____/ &lt;span class="se"&gt;\_&lt;/span&gt;___/ &lt;span class="se"&gt;\_&lt;/span&gt;____________/ &lt;span class="se"&gt;\_&lt;/span&gt;_/ &lt;span class="se"&gt;\_&lt;/span&gt;______/ &lt;span class="se"&gt;\_&lt;/span&gt;____________/
  scheme      user    pass        host       port  database    parameters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;scheme&lt;/strong&gt; - &lt;code&gt;postgresql://&lt;/code&gt; (or &lt;code&gt;postgres://&lt;/code&gt;; equivalent).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;user:password&lt;/strong&gt; - the database role and its password. Reserved characters in either must be percent-encoded (see FAQ - this causes real outages).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;host&lt;/strong&gt; - hostname or IP of the server. &lt;code&gt;localhost&lt;/code&gt; in dev; a provider hostname in production. (Inside Docker Compose, it's the &lt;em&gt;service name&lt;/em&gt; - &lt;code&gt;db&lt;/code&gt;, not localhost - a classic gotcha.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;port&lt;/strong&gt; - 5432 is Postgres's default; poolers sometimes listen elsewhere (6432 is a PgBouncer convention).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;database&lt;/strong&gt; - one server hosts many databases; this picks yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parameters&lt;/strong&gt; - &lt;code&gt;?key=value&amp;amp;key=value&lt;/code&gt; options, of which a handful matter constantly:&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The parameters that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sslmode&lt;/code&gt; - whether the connection is encrypted and verified. Set it explicitly, always; &lt;code&gt;require&lt;/code&gt; is the pragmatic floor and &lt;code&gt;verify-full&lt;/code&gt; the strict goal - the differences are their own article.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;connect_timeout&lt;/code&gt; - seconds to wait before giving up; a small value (5-10) turns network problems into fast, clear errors instead of hangs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;application_name&lt;/code&gt; - labels your app's sessions in pg_stat_activity, which future-you will bless during any connection investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pool sizing&lt;/strong&gt; - lives in your driver/ORM config rather than the URL (e.g. Prisma's &lt;code&gt;connection_limit&lt;/code&gt; URL param being a notable exception). Either way: configured deliberately, &lt;a href="https://dev.to/blog/postgresql-connection-pooling-explained-pgbouncer"&gt;not defaulted&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the string lives
&lt;/h2&gt;

&lt;p&gt;In the environment - never in code, never in git. The pattern every framework supports: read &lt;code&gt;process.env.DATABASE_URL&lt;/code&gt; / &lt;code&gt;os.environ["DATABASE_URL"]&lt;/code&gt;, provide it via a gitignored &lt;code&gt;.env&lt;/code&gt; locally and the platform's secret store in production, and commit only an &lt;code&gt;.env.example&lt;/code&gt; documenting the shape. One variable per environment is the entire multi-env story: dev points at local Docker Postgres, staging and prod at their own databases, and the code never knows the difference. This one-variable portability is also why migrating providers is a config change: on &lt;a href="https://dev.to/postgres"&gt;Swyftstack&lt;/a&gt;, the dashboard hands you the full URL with SSL parameters included - paste, deploy, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four errors everyone meets
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;connection refused&lt;/code&gt; - nothing listening at host:port from where you are: wrong host (localhost inside a container?), wrong port, database not running, or a firewall. It's a network statement, not an auth one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;password authentication failed&lt;/code&gt; - reached the server, wrong credentials: user/password mismatch, or the percent-encoding trap mangling both.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;database "x" does not exist&lt;/code&gt; - reached and authenticated; the path component names a database nobody created. Create it, or fix the name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;no pg_hba.conf entry / SSL off&lt;/code&gt; - the server demands TLS you didn't offer: add the right &lt;code&gt;sslmode&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Debugging tip: test the exact string in isolation with &lt;code&gt;psql "$DATABASE_URL" -c 'select 1'&lt;/code&gt; - it removes your app from the equation and turns 'the app can't connect' into a precise, googleable error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the URL: how frameworks consume it
&lt;/h2&gt;

&lt;p&gt;Knowing the anatomy, the framework-specific consumption is quick. Prisma reads &lt;code&gt;env("DATABASE_URL")&lt;/code&gt; in its datasource block and layers its own URL parameters (&lt;code&gt;connection_limit&lt;/code&gt;, &lt;code&gt;pool_timeout&lt;/code&gt;) on top. Django wants the pieces separated, which &lt;code&gt;dj_database_url.parse()&lt;/code&gt; does in one call. Rails reads DATABASE_URL natively and merges it with database.yml. Node's pg accepts the whole string as &lt;code&gt;connectionString&lt;/code&gt;. SQLAlchemy takes it as the engine URL, though async drivers want the scheme spelled &lt;code&gt;postgresql+asyncpg://&lt;/code&gt; - the one place the flexible scheme prefix actually matters. In every case the pattern holds: the URL is the single source of truth, and framework config decorates rather than replaces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple databases, one convention
&lt;/h2&gt;

&lt;p&gt;Apps grow second databases - a read replica, an analytics copy, a queue. Resist inventing per-case formats: the convention that scales is one URL per purpose (&lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;REPLICA_URL&lt;/code&gt;, &lt;code&gt;ANALYTICS_DATABASE_URL&lt;/code&gt;), each complete and self-contained, each documented in &lt;code&gt;.env.example&lt;/code&gt;. Deriving URLs in code - swapping hostnames, rewriting ports - is the road to a staging app quietly writing to production; complete, explicit URLs make every connection target reviewable at a glance. The same one-URL-per-purpose rule is what makes provider migrations a config change and local Docker development identical in shape to production - the humble env var doing quiet architectural work.&lt;/p&gt;

&lt;p&gt;For the genuinely-beginner readers who made it here: don't be discouraged that a single line of configuration carried this much explanation. The connection string is where networking, authentication, encryption, and application config all meet - which is exactly why it's the most common thing to get wrong on a first deployment, and why understanding it pays off across every backend technology you'll ever touch. Every database, message queue, and cache you meet from here uses the same URL grammar; you've just learned all of them at once.&lt;/p&gt;

&lt;p&gt;Three parameters we skipped that you'll meet eventually, so they're not strangers: pgbouncer=true (some poolers/ORMs want a hint that a transaction-mode pooler is in the path), options=-c%20statement_timeout%3D5000 (session settings smuggled through the URL, percent-encoded), and target_session_attrs=read-write (multi-host strings that prefer the writable primary - yes, a URL can list several hosts). Each is niche; each has saved someone a day when they knew it existed.&lt;/p&gt;

&lt;p&gt;A closing exercise that cements it: open your current project's DATABASE_URL and narrate every segment aloud - who connects, to where, on what port, to which database, under what encryption. If any segment draws a blank, this article's relevant section is a scroll away. Being able to read the line fluently is a small skill with an outsized payoff: it turns the most common category of deployment failure into something you diagnose in seconds rather than search in panic.&lt;/p&gt;

&lt;p&gt;The same literacy also transfers sideways: Redis URLs, AMQP strings, SMTP DSNs, and MongoDB connection strings all follow the identical scheme-credentials-host-database-parameters grammar with different vocabularies. Learning to read one URL family fluently means never squinting at any of them again - a rare case of a fifteen-minute skill covering an entire category of tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A connection string is your database's address, keys, and safety rules in one line. Learn to read it once, set sslmode explicitly, keep it out of git - and half of backend debugging becomes translation instead of mystery.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>fullstack</category>
      <category>programming</category>
      <category>database</category>
    </item>
    <item>
      <title>PostgreSQL EXPLAIN ANALYZE: Read Plans, Fix Slow Queries</title>
      <dc:creator>Akshay</dc:creator>
      <pubDate>Tue, 14 Jul 2026 18:27:41 +0000</pubDate>
      <link>https://dev.to/akshay272727/postgresql-explain-analyze-read-plans-fix-slow-queries-4bk</link>
      <guid>https://dev.to/akshay272727/postgresql-explain-analyze-read-plans-fix-slow-queries-4bk</guid>
      <description>&lt;p&gt;Slow database queries rarely announce themselves. The app just feels sluggish, one endpoint times out under load, and everyone suspects the framework, the network, or the phase of the moon. PostgreSQL will tell you precisely what's slow and why - if you ask it correctly. This is the workflow we use: find the offenders with &lt;code&gt;pg_stat_statements&lt;/code&gt;, understand them with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;, and fix them with one of three well-aimed changes.&lt;/p&gt;

&lt;p&gt;![In a real plan, one node usually owns nearly all the time - find it, then fix that node.]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2jflgyzq1yqgnf2qeh1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2jflgyzq1yqgnf2qeh1k.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In a real plan, one node usually owns nearly all the time - find it, then fix that node.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EXPLAIN ANALYZE&lt;/strong&gt; is the single most useful diagnostic command in Postgres. Plain &lt;code&gt;EXPLAIN&lt;/code&gt; prints the plan the query planner &lt;em&gt;intends&lt;/em&gt; to run, with cost estimates. Add &lt;code&gt;ANALYZE&lt;/code&gt; and Postgres actually executes the query and reports the &lt;strong&gt;real&lt;/strong&gt; timing and row count at every step of the plan - so you can see exactly where the wall-clock time goes instead of guessing. (Because it really runs the query, use it on a replica or inside a rolled-back transaction when the statement writes.) The rest of this post is how to read that output and act on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Find the queries that actually matter
&lt;/h2&gt;

&lt;p&gt;Don't guess from application logs. The &lt;code&gt;pg_stat_statements&lt;/code&gt; extension aggregates every query's execution count and total time. Enable it once, then ask for the top offenders by &lt;em&gt;total&lt;/em&gt; time - a metric that automatically balances 'very slow' against 'very frequent':&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="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_exec_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mean_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output almost always surprises. The 'slow report query' everyone blames is frequently nowhere near the top; the real cost is a mid-speed query on a hot path, or an N+1 pattern where the ORM fires the same 5 ms query five hundred times per request. Fix in order of &lt;code&gt;total_ms&lt;/code&gt;, top to bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Read the plan, not the tea leaves
&lt;/h2&gt;

&lt;p&gt;Take the worst query and run it under EXPLAIN ANALYZE with buffers enabled:&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;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&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="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to understand every node type. Scan for three signals, in priority order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**Seq Scan on a large table. **PostgreSQL read every row because no usable index existed. If the filter is selective (returns a small fraction of rows), this is your fix - add the index.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Estimated vs actual rows wildly different. *&lt;/em&gt;'rows=100' estimated but 'actual rows=2,000,000' means the planner is flying blind on stale statistics. Run &lt;code&gt;ANALYZE tablename;&lt;/code&gt; and re-plan before touching anything else.&lt;/li&gt;
&lt;li&gt;**Nested Loop with a huge inner count. **A join executed row-by-row thousands of times, usually downstream of a bad estimate or a missing index on the join key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Apply the right fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix A: the missing or wrong index
&lt;/h3&gt;

&lt;p&gt;Most slow queries are one index away from fast. Match the index to the query's shape: equality columns first, then range columns. For the query above:&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="c1"&gt;-- status is an equality filter, created_at a range filter:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_status_created&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CONCURRENTLY&lt;/code&gt; builds the index without locking writes - essential on a production table. Re-run the EXPLAIN afterwards and confirm the Seq Scan became an Index Scan and the runtime dropped. If it didn't, the planner had a reason; check the estimate mismatch signal again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix B: reshape the query
&lt;/h3&gt;

&lt;p&gt;When an index exists but can't be used, the query shape is usually at fault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Functions on indexed columns - &lt;code&gt;WHERE lower(email) = $1&lt;/code&gt; needs an expression index on &lt;code&gt;lower(email)&lt;/code&gt;, or store the value normalised.&lt;/li&gt;
&lt;li&gt;N+1 loops - replace per-row lookups with one query using &lt;code&gt;WHERE id = ANY($1)&lt;/code&gt; or a join. This is an application change, but it's the single biggest win in ORM-heavy codebases.&lt;/li&gt;
&lt;li&gt;Overfetching - &lt;code&gt;SELECT *&lt;/code&gt; on wide tables drags TOASTed columns and blocks index-only scans. Name the columns.&lt;/li&gt;
&lt;li&gt;OFFSET pagination on deep pages - &lt;code&gt;OFFSET 100000&lt;/code&gt; reads and discards 100k rows. Use keyset pagination (&lt;code&gt;WHERE id &amp;gt; $last LIMIT 50&lt;/code&gt;) instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix C: give the planner fresh statistics
&lt;/h3&gt;

&lt;p&gt;PostgreSQL plans queries using sampled statistics that autovacuum refreshes opportunistically. After bulk loads, mass deletes, or on very fast-growing tables, those samples lag reality and the planner picks catastrophic plans with full confidence. &lt;code&gt;ANALYZE&lt;/code&gt; is instant and safe to run any time; if a table's stats lag chronically, lower its &lt;code&gt;autovacuum_analyze_scale_factor&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it a habit, not a heroic
&lt;/h2&gt;

&lt;p&gt;The teams with fast databases aren't smarter - they just look at the top-10 query list monthly and after every feature launch, when a regression is one commit old instead of one quarter old. A managed platform should make this loop effortless: Swyftstack's &lt;a href="https://swyftstack.com/postgres" rel="noopener noreferrer"&gt;managed PostgreSQL&lt;/a&gt; surfaces query rate, active connections, and a slow query log on the dashboard, so step 1 is a glance instead of an extension install. And because it's standard PostgreSQL, every EXPLAIN technique in this post works unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a real plan, line by line
&lt;/h2&gt;

&lt;p&gt;Here is the kind of output that intimidates people, trimmed to the parts that matter. Suppose the orders query above produces this plan fragment:&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="n"&gt;Nested&lt;/span&gt; &lt;span class="n"&gt;Loop&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;5205&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;5100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
       &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;318&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;customers_pkey&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three readings jump out once you know where to look. The Seq Scan on orders consumed 5.1 of the 5.2 total seconds - that line IS the problem; everything else is noise. 'Rows Removed by Filter: 4.3 million' tells you the scan read the whole table to keep 1,842 rows - a selectivity of 0.04%, which is exactly the profile an index exists for. And the customers side is already healthy: an index scan executed 1,842 times at 0.05 ms each is the nested loop working as designed. The fix writes itself - the composite index on (status, created_at) from Fix A - and the re-run plan should show an Index Scan on orders with the total time dropping from seconds to single-digit milliseconds.&lt;/p&gt;

&lt;p&gt;This is the general skill: do not read plans top to bottom like prose. Find the node that owns the wall-clock time, check whether its actual row count embarrasses its estimate, and ask what access path would have served its filter. Ninety percent of plan-reading is those three questions applied to one node.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One index, one N+1 fix, and one ANALYZE resolve the overwhelming majority of slow-query incidents. The skill is knowing which of the three you're looking at - and EXPLAIN ANALYZE tells you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  PostgreSQL query analyzer tools: what's worth using
&lt;/h2&gt;

&lt;p&gt;People searching for a 'PostgreSQL query analyzer' usually want one of three different tools, and EXPLAIN ANALYZE is only the first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EXPLAIN ANALYZE&lt;/strong&gt; - the built-in plan analyzer this guide covers. Free, always available, answers 'why is &lt;code&gt;this&lt;/code&gt; query slow'.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pg_stat_statements&lt;/strong&gt; - the built-in workload analyzer. It aggregates timing across every query the server runs, so it answers the prior question: 'which query should I even be looking at?' Enable it before you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualizers&lt;/strong&gt; - tools like explain.depesz.com and explain.dalibo.com turn a pasted plan into an annotated tree, which is the fastest way to read large plans. Your data stays in the plan text you choose to paste.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good managed host wires the first two in for you - &lt;a href="https://swyftstack.com/postgres" rel="noopener noreferrer"&gt;Swyftstack's managed Postgres&lt;/a&gt; ships with pg_stat_statements available, and our &lt;a href="https://swyftstack.com/blog/best-postgresql-hosting-providers" rel="noopener noreferrer"&gt;PostgreSQL hosting comparison&lt;/a&gt; covers what else to expect from a provider on the observability front.&lt;/p&gt;

</description>
      <category>database</category>
      <category>webdev</category>
      <category>postgres</category>
      <category>devops</category>
    </item>
    <item>
      <title>PostgreSQL Connection Pool Sizing: The Formula That Works</title>
      <dc:creator>Akshay</dc:creator>
      <pubDate>Tue, 14 Jul 2026 18:20:00 +0000</pubDate>
      <link>https://dev.to/akshay272727/postgresql-connection-pool-sizing-the-formula-that-works-303i</link>
      <guid>https://dev.to/akshay272727/postgresql-connection-pool-sizing-the-formula-that-works-303i</guid>
      <description>&lt;p&gt;Almost every question about connection pooling eventually collapses into one practical number: &lt;strong&gt;how big should the pool be?&lt;/strong&gt; Get pool sizing wrong in one direction and you meet &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt; during a traffic spike. &lt;/p&gt;

&lt;p&gt;Get it wrong in the other direction - a pool that's far too large - and you quietly make the database &lt;em&gt;slower&lt;/em&gt; under load, for reasons most people never suspect. &lt;/p&gt;

&lt;p&gt;This post gives you the sizing formula that actually works, the PgBouncer math behind it, and the two things you have to understand first for the numbers to make sense: why Postgres connections are so expensive, and the transaction-versus-session-mode choice that decides how much a pooler can multiplex.&lt;/p&gt;

&lt;p&gt;![Many app connections share a small PgBouncer pool sized (cores x 2) + 1 into a few Postgres backends.]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mvro1r6l8w1l2yh7rpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mvro1r6l8w1l2yh7rpj.png" alt=" " width="799" height="407"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Many app connections share a small PgBouncer pool sized (cores x 2) + 1 into a few Postgres backends.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you just want the short answer: a good starting point for the pool that talks to Postgres is roughly &lt;code&gt;(cpu_cores x 2) + 1&lt;/code&gt; connections - often a low-tens number, not the hundreds people reach for. The rest of this post explains why that formula holds, how to translate it into PgBouncer's &lt;code&gt;default_pool_size&lt;/code&gt; and &lt;code&gt;max_client_conn&lt;/code&gt;, and when you can skip a dedicated pooler entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why PostgreSQL runs out of connections
&lt;/h2&gt;

&lt;p&gt;Every PostgreSQL connection is backed by its own operating-system process on the server, with its own memory. That design makes Postgres robust and isolated, but it also makes connections &lt;strong&gt;expensive and finite&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;A typical instance is configured for a few hundred connections via &lt;code&gt;max_connections&lt;/code&gt;, and each idle connection still costs memory. So the ceiling is lower than people assume - you don't need thousands of concurrent queries to hit it, just thousands of mostly-idle &lt;em&gt;connections&lt;/em&gt; that someone forgot to close.&lt;/p&gt;

&lt;p&gt;Two patterns blow through that ceiling fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**Connection-per-request code. **An app that opens a new connection for every HTTP request and doesn't reuse a pool will stack up connections under load.&lt;/li&gt;
&lt;li&gt;**Serverless sprawl. **On Lambda, Vercel functions, or Cloudflare Workers, each concurrent invocation is effectively its own process. A thousand concurrent invocations, each opening even one connection, is a thousand connections - a stampede the database can't absorb.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What a connection pooler actually does
&lt;/h2&gt;

&lt;p&gt;A pooler like &lt;strong&gt;PgBouncer&lt;/strong&gt; (or Supabase's Supavisor, or pgcat) sits between your application and PostgreSQL. Your app opens lots of cheap connections to the pooler; the pooler maintains a small set of real, expensive connections to Postgres and &lt;strong&gt;multiplexes&lt;/strong&gt; your traffic across them. A thousand app connections can share a pool of, say, twenty backend connections, because at any given instant most of them aren't actually running a query. The pooler is essentially a traffic controller that keeps the expensive resource - real Postgres connections - busy and bounded.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A pooler doesn't make Postgres faster. It makes a small number of connections serve a large number of clients, which is exactly what you need when you have far more clients than the database can hold.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Transaction mode vs session mode - the choice that matters
&lt;/h2&gt;

&lt;p&gt;PgBouncer offers a few pooling modes, but two matter in practice, and picking the wrong one causes subtle bugs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Session mode
&lt;/h3&gt;

&lt;p&gt;In session mode, a client holds a backend connection for the entire duration of its session - from connect to disconnect. This is the most compatible mode: everything that relies on connection-level state works normally, including &lt;code&gt;SET&lt;/code&gt; settings, &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;, session-level advisory locks, and named prepared statements. The downside is lower multiplexing - one client ties up one backend connection for as long as it's connected, so you get less of the pooling benefit.&lt;/p&gt;
&lt;h3&gt;
  
  
  Transaction mode
&lt;/h3&gt;

&lt;p&gt;In transaction mode, a backend connection is assigned to a client only for the duration of a single transaction, then returned to the pool. This gives you &lt;strong&gt;maximum multiplexing&lt;/strong&gt; - ideal for serverless and high-concurrency apps - because a connection is held for milliseconds, not minutes. The trade-off: because you might get a different backend connection for the next transaction, &lt;strong&gt;anything that depends on session state breaks&lt;/strong&gt;. That includes session-level &lt;code&gt;SET&lt;/code&gt;, &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt;, and - the classic footgun - server-side &lt;strong&gt;named prepared statements&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The prepared-statement gotcha (a true story shape)
&lt;/h2&gt;

&lt;p&gt;Here's the bug that catches everyone. Many drivers and ORMs use named prepared statements under the hood (Prisma is a well-known example). In transaction mode, prepared statement &lt;code&gt;s0&lt;/code&gt; might be created on one backend connection and then reused on a different one that's never heard of it - producing errors like &lt;code&gt;prepared statement "s0" already exists&lt;/code&gt; (SQLState 42P05) or &lt;code&gt;prepared statement "s0" does not exist&lt;/code&gt;. The fix is to tell your driver to disable named prepared statements when talking through a transaction-mode pooler (for example, the &lt;code&gt;pgbouncer=true&lt;/code&gt; flag on the connection string for some clients), or to use the pooler only for the runtime app and the direct connection for migrations and admin tasks.&lt;/p&gt;
&lt;h2&gt;
  
  
  A practical rule for which connection to use where
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;**Runtime app traffic: **use the pooler (transaction mode for serverless, session mode for long-running servers that need session features).&lt;/li&gt;
&lt;li&gt;**Migrations, schema changes, pg_dump, seeds: **use the direct (non-pooled) connection. These need session state and prepared statements, and they're not high-concurrency.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Connection pool sizing: the formula that actually works
&lt;/h2&gt;

&lt;p&gt;Pool sizing is where intuition misleads almost everyone. The instinct is 'more traffic, so make the pool bigger'. But the pool that matters most - the one from your pooler (or app) to Postgres - should be sized to the &lt;strong&gt;database's ability to do work in parallel&lt;/strong&gt;, not to how many clients are knocking. Once every CPU core is busy running a query, adding more concurrent connections doesn't do more work; it just adds lock contention, context-switching, and memory pressure. Past that point a bigger pool makes throughput &lt;em&gt;worse&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The widely-used starting formula, popularised by the PostgreSQL wiki and the HikariCP project, is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connections &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;cpu_cores x 2&lt;span class="o"&gt;)&lt;/span&gt; + effective_spindle_count


&lt;span class="c"&gt;# On modern SSD/NVMe systems the spindle term is small, so the&lt;/span&gt;
&lt;span class="c"&gt;# practical rule of thumb collapses to:&lt;/span&gt;

connections &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;cpu_cores x 2&lt;span class="o"&gt;)&lt;/span&gt; + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a 4-core Postgres instance wants a backend pool in the neighbourhood of &lt;strong&gt;9 connections&lt;/strong&gt;, not 90. That feels far too small until you internalise the key insight: a pool of 9 that stays busy will out-throughput a pool of 100 that spends its time fighting over the same cores. Start near the formula, then measure and adjust - if CPU is underused and queries are waiting on the pool, nudge it up; if CPU is pinned and latency is climbing, it's already too big.&lt;/p&gt;

&lt;h3&gt;
  
  
  Turning the number into PgBouncer settings
&lt;/h3&gt;

&lt;p&gt;PgBouncer exposes a few knobs, and the sizing formula maps onto them directly. The three that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;*&lt;em&gt;default_pool_size *&lt;/em&gt;- the number of real backend connections PgBouncer opens to Postgres per (user, database) pair. This is where your &lt;code&gt;(cores x 2) + 1&lt;/code&gt; number goes. It defaults to 20.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;max_client_conn *&lt;/em&gt;- how many application connections PgBouncer will accept out front. Because pooled client connections are cheap, this is deliberately large, often in the thousands.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;reserve_pool_size *&lt;/em&gt;- a small emergency pool that kicks in when a regular pool is exhausted, to absorb short bursts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hard constraint to respect: the sum of every pool's &lt;code&gt;default_pool_size&lt;/code&gt; across all (db, user) pairs must stay &lt;strong&gt;below Postgres's own&lt;/strong&gt; &lt;code&gt;max_connections&lt;/code&gt;, with 10-20 connections left as headroom for superuser access, replication, and monitoring. If PgBouncer is allowed to open more backend connections than Postgres permits, you've just moved the 'too many clients' error one layer down instead of fixing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A worked example
&lt;/h3&gt;

&lt;p&gt;A single 4-core Postgres instance serving one application database and one role:&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;# postgresql.conf&lt;/span&gt;
max_connections &lt;span class="o"&gt;=&lt;/span&gt; 100          &lt;span class="c"&gt;# plenty of headroom above the pool&lt;/span&gt;

&lt;span class="c"&gt;# pgbouncer.ini&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;databases]
app &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5432 &lt;span class="nv"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app

&lt;span class="o"&gt;[&lt;/span&gt;pgbouncer]
pool_mode &lt;span class="o"&gt;=&lt;/span&gt; transaction        &lt;span class="c"&gt;# max multiplexing for a web app&lt;/span&gt;
default_pool_size &lt;span class="o"&gt;=&lt;/span&gt; 20         &lt;span class="c"&gt;# backend conns to Postgres (near (4x2)+1, rounded up for burst)&lt;/span&gt;
max_client_conn &lt;span class="o"&gt;=&lt;/span&gt; 2000         &lt;span class="c"&gt;# cheap app-side connections out front&lt;/span&gt;
reserve_pool_size &lt;span class="o"&gt;=&lt;/span&gt; 5          &lt;span class="c"&gt;# short-burst headroom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That configuration lets up to 2,000 application connections share a pool of ~20 real backend connections - and 20 sits comfortably under the database's 100-connection ceiling. Two thousand clients, twenty expensive resources, one database that never sees 'sorry, too many clients already'.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sizing the pool inside your application
&lt;/h3&gt;

&lt;p&gt;The pooler isn't the only pool - your app has one too, and the right size depends entirely on your runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**Long-running server (Express, a worker): **one shared pool per process, with a modest max (think 10-20), reused across all requests. Size the total across all processes to fit under the database or PgBouncer ceiling.&lt;/li&gt;
&lt;li&gt;**Serverless function: **a tiny pool per instance (1-2 connections), with a transaction-mode pooler doing the real multiplexing in front of Postgres. A thousand warm instances each holding 1-2 connections is exactly the stampede a pooler exists to absorb.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake to avoid is creating a new client or pool on every request or every cold start. Create the pool once at module scope, reuse it everywhere, attach an error handler, and close it cleanly on shutdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you even need a pooler?
&lt;/h2&gt;

&lt;p&gt;If you run a single long-lived server with a sensible shared pool and your concurrency is modest, you may never hit the ceiling - a pooler adds a component you don't need. You almost certainly &lt;em&gt;do&lt;/em&gt; need one if you're serverless, if you run many app instances, or if you've already met the 'too many clients' error. The honest answer is: pool inside your app first, add a dedicated pooler when your deployment model multiplies connections beyond what the database can hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Swyftstack fits
&lt;/h2&gt;

&lt;p&gt;Pooling is one of those things that's easy to get wrong and tedious to operate. Swyftstack ships &lt;a href="https://swyftstack.com/postgres" rel="noopener noreferrer"&gt;managed PostgreSQL&lt;/a&gt; with pooling configured for you, so you can pick transaction or session mode without standing up and tuning PgBouncer yourself. You still get a standard connection string and real, unmodified Postgres - the pooler is just handled. If you're building on &lt;a href="https://dev.to/nextjs-database"&gt;Next.js or another serverless runtime&lt;/a&gt;, that means the 'too many clients' error is one less thing you have to design around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Connection pooling exists because Postgres connections are heavyweight and your app wants to open lots of them. A pooler multiplexes many cheap client connections onto a few real backend ones. Use transaction mode for serverless (and disable named prepared statements), session mode when you need connection-level features, the direct connection for migrations, and a modest, reused pool everywhere. Get that right and you'll never see 'sorry, too many clients already' again.&lt;/p&gt;

&lt;h2&gt;
  
  
  PgBouncer pool-size settings, in one table
&lt;/h2&gt;

&lt;p&gt;The settings people actually search for, and what each one really controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;th&gt;Sane starting point&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;default_pool_size&lt;/td&gt;
&lt;td&gt;Server connections per user+database pair&lt;/td&gt;
&lt;td&gt;(cores x 2) + 1 on the Postgres box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;min_pool_size&lt;/td&gt;
&lt;td&gt;Connections kept warm even when idle&lt;/td&gt;
&lt;td&gt;0, or a small number if cold bursts hurt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_client_conn&lt;/td&gt;
&lt;td&gt;Client connections PgBouncer itself accepts&lt;/td&gt;
&lt;td&gt;High (1,000+) - clients are cheap, servers are not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_db_connections&lt;/td&gt;
&lt;td&gt;Hard cap on server connections per database&lt;/td&gt;
&lt;td&gt;Below Postgres max_connections, leaving admin headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pool_mode&lt;/td&gt;
&lt;td&gt;When a server connection is released&lt;/td&gt;
&lt;td&gt;transaction, unless you need session features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reserve_pool_size&lt;/td&gt;
&lt;td&gt;Emergency connections under burst&lt;/td&gt;
&lt;td&gt;A handful; alerts should fire before it's used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The recurring mistake is raising &lt;code&gt;default_pool_size&lt;/code&gt; to fix queueing that is actually slow queries holding connections - measure with &lt;code&gt;SHOW POOLS&lt;/code&gt; before turning any of these dials. If you are still choosing where the database itself should live, our &lt;a href="https://swyftstack.com/blog/best-postgresql-hosting-providers" rel="noopener noreferrer"&gt;PostgreSQL hosting comparison&lt;/a&gt; notes which providers bundle a pooler (several do, including &lt;a href="https://swyftstack.com/postgres" rel="noopener noreferrer"&gt;Swyftstack&lt;/a&gt;) so these settings arrive pre-tuned.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>webdev</category>
      <category>development</category>
      <category>database</category>
    </item>
  </channel>
</rss>
