Writing
Database
Mingtindu Sherpa9 min read

PostgreSQL Connection Pooling: Direct Connection vs Pooler

Compare direct PostgreSQL connections, application-side pools, and external poolers for persistent Node.js, serverless, Prisma, and migration workloads.

On this page

A direct PostgreSQL connection gives one client a dedicated server session. A client-side pool reuses several such sessions inside one application process. An external pooler sits between many clients and PostgreSQL, allowing database sessions to be shared according to its pooling mode.

Pooling can protect a finite connection budget and reduce repeated setup cost. It does not automatically improve every workload: an extra network hop, queueing, timeouts, and loss of session behavior can make the wrong pooling mode worse.

What a PostgreSQL connection represents

PostgreSQL uses a server process for each connected client. That session consumes memory and server resources even while it is idle, and it can hold transaction state, prepared statements, temporary objects, settings, advisory locks, and notification subscriptions.

max_connections limits concurrent connections. Some slots are reserved, and managed providers impose plan or role limits as well. Raising the limit is not a free capacity upgrade; it increases the amount of work the server may be asked to handle concurrently.

Three connection types

Connection typeTypical useAdvantagesLimitations
DirectMigrations and persistent workloadsFull session behaviorHigher database connection usage
Client-side poolLong-running Node.js processReuse inside one processEach instance owns a pool
External poolerServerless and many instancesControls database connectionsTransaction-mode limitations

These layers can be combined. A Node.js process may have a small client-side pool whose connections point to an external pooler. The combined concurrency limits and queueing behavior must still be understood.

Direct connections

A direct endpoint connects the application or tool to PostgreSQL without a separate transaction pooler. It preserves session-level behavior and is commonly appropriate for:

  • migration and administrative commands;
  • pg_dump, pg_restore, and introspection;
  • LISTEN/NOTIFY;
  • session advisory locks, temporary tables, or session-level SET values;
  • a bounded number of persistent application processes whose total pool sizes fit the database budget.

Port 5432 is PostgreSQL's conventional default, but a URL on port 5432 is not proof that it is direct. Proxies and hosted poolers can also listen there. Use the endpoint label and documentation supplied by the operator.

Client-side pools in persistent Node.js

A client-side pool keeps a bounded set of connections ready inside one process. For node-postgres, create a shared Pool rather than a new client or pool for every request:

import { Pool } from "pg";
 
export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  connectionTimeoutMillis: 5_000,
  idleTimeoutMillis: 30_000,
});

Those numbers are illustrative, not universal recommendations. Pool size must account for every application instance, workers, deployment overlap, administrative access, and reserved capacity:

possible application connections = instance count × pool max

Ten instances with a maximum of ten connections can request up to one hundred application connections. Autoscaling to fifty instances changes the upper bound to five hundred even though no code changed.

Persistent Node.js servers benefit because a process can reuse its pool over many requests. Acquire a client only when a transaction needs one session, release it in a finally block, and listen for background pool errors. A leaked client gradually empties the available pool.

External poolers

An external service such as PgBouncer or a provider-managed pooler accepts client connections and assigns PostgreSQL backend connections according to a mode.

Session pooling

A backend connection remains assigned for the client session. Session state works much like a direct connection, but backend reuse happens only when the client disconnects. This reduces connection churn more than it reduces the number of simultaneously active sessions.

Transaction pooling

A backend connection is assigned for one transaction and then returned to the pool. This lets many client sessions share fewer PostgreSQL connections, which is useful for short-lived or highly autoscaled applications.

The client cannot assume its next transaction gets the same backend. Session-level state, temporary tables, session advisory locks, LISTEN, and some prepared-statement workflows may fail or disappear across transaction boundaries. Compatibility depends on the pooler, its version/configuration, the database driver, and ORM behavior.

Ports are conventions, not guarantees

Port 5432 is commonly direct PostgreSQL, while 6543 is used by some hosted transaction poolers. Neither meaning is universal.

Supabase is a concrete example: its current documentation uses 5432 for the direct endpoint and its shared session pooler, while shared transaction mode uses 6543. That alone shows why changing a port based on a generic rule is unsafe. Copy the exact direct, session, or transaction URL from the provider's connection panel.

Use the PostgreSQL Connection URL Builder to assemble known endpoint details; it cannot discover which port a provider assigned.

Persistent servers versus serverless instances

A long-running server can reuse one process pool for many requests. Size the pool across the known instance count and allow headroom for deployments and maintenance.

Serverless and autoscaling platforms can create many isolated instances. Each warm instance may own its own client-side pool, so a modest per-instance default can still exceed the database limit during a burst. Reuse a client outside the handler where the runtime supports warm reuse, cap per-instance pools/concurrency, or use a provider-supported external pooler.

Serverless is not automatically transaction-pooled, and every pooler is not automatically compatible with every serverless runtime. Confirm transport support, connection lifetime, concurrency, idle behavior, and provider limits.

Prisma 6 and Prisma 7 differ

This repository does not install Prisma, so there is no local Prisma version or runtime configuration to inspect.

In Prisma ORM 6 and earlier, the built-in relational pool accepted URL parameters such as connection_limit and pool_timeout. In Prisma ORM 7, relational databases use driver adapters by default; PostgreSQL pooling comes from the supplied pg driver. The equivalent settings include max, connectionTimeoutMillis, and idleTimeoutMillis, and their defaults differ from Prisma 6.

Do not copy a connection_limit=1 URL tuning rule into Prisma 7 and assume it controls the driver adapter. Pin the Prisma and adapter versions, then use their current configuration reference.

Create one PrismaClient instance per long-running process where the framework permits it. In development with hot reload, follow Prisma's framework-specific singleton guidance so module reloads do not accumulate clients.

Runtime and migration connections

Separating runtime and migration endpoints is provider- and version-specific. Current Prisma Postgres documentation supplies a pooled URL for application queries and a direct URL for CLI/admin work. Current Supabase guidance similarly recommends its direct endpoint for migrations and transaction mode for temporary serverless clients.

A representative Prisma 7 configuration is:

import "dotenv/config";
import { defineConfig, env } from "prisma/config";
 
export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: env("DIRECT_DATABASE_URL"),
  },
});
# Placeholder formats only; copy actual endpoints from the provider.
DATABASE_URL="postgresql://app_user:demo_password@pool.db.example.com:6543/example_database?sslmode=require"
DIRECT_DATABASE_URL="postgresql://app_user:demo_password@db.example.com:5432/example_database?sslmode=require"

The port and hostnames above are illustrative, not provider facts. Runtime code would configure its driver/adapter with DATABASE_URL, while the CLI configuration uses the reviewed direct URL. Never print either.

For command roles and deployment workflow, read Prisma Migrate Dev vs Deploy vs DB Push. For transport protection, read PostgreSQL SSL Modes Explained for Node.js and Prisma.

Prepared statements and transaction poolers

Prepared statements can be session-scoped. A transaction pooler may send a later operation to a different backend, where the prepared statement does not exist or a name collides.

Some modern poolers support protocol-level prepared statements with specific configuration; some providers tell Prisma clients to add a compatibility parameter or disable prepared statements. Treat this as a provider-and-version matrix, not a blanket statement that prepared statements always work or never work. Supabase currently documents pgbouncer=true for Prisma through its transaction-mode Supavisor endpoint.

Connection limits and pool timeouts

A pool limit caps how many backend connections one application pool may open. A pool or acquire timeout controls how long work waits for an available connection before failing. Driver connection timeouts and query/transaction timeouts are separate.

If the pool is saturated:

  • a bounded wait can fail quickly and preserve application resources;
  • an unlimited wait can allow requests to pile up;
  • a very large pool can move the queue into PostgreSQL and overload it;
  • aggressive retries can amplify the incident.

Define failure behavior: time budgets, retryable versus non-retryable operations, idempotency, load shedding, and user-visible errors. Pooling cannot replace application backpressure.

Monitor the complete path

Monitor at least:

  • active, idle, and waiting clients in the application pool;
  • pool acquisition latency and timeout counts;
  • provider pooler client/server connection counts;
  • PostgreSQL pg_stat_activity, connection utilization, wait events, and long transactions;
  • autoscaling instance count and deployment overlap;
  • errors such as Prisma P2024, "too many connections," broken connections, and prepared-statement failures.

Do not log connection strings while adding diagnostics. Label metrics with safe service and environment names, not hosts, users, or database URLs.

Decision checklist

  • Identify whether each endpoint is direct, session pooled, or transaction pooled from official provider documentation.
  • Budget application pools across maximum instance count, not only one process.
  • Preserve headroom for operators, migrations, monitoring, and deployment overlap.
  • Confirm session features and prepared statements are compatible with the selected mode.
  • Use the documented direct/admin endpoint for migration commands when the provider requires it.
  • Configure bounded timeouts and explicit overload behavior.
  • Monitor both client-side and server-side pool saturation.
  • Re-check Prisma 6 versus 7 configuration before copying URL parameters.

References

Documentation checked on 2026-08-08:

Related writing

Share