The short answer
- Serverless or edge: shared transaction pooler, port
6543, prepared statements off. - Persistent app on IPv4: shared session pooler, port
5432. - IPv6 network or IPv4 add-on: direct connection, port
5432. - Paid, high-performance app: dedicated pooler, port
6543, transaction mode only. - Migrations,
pg_dump, schema tools: direct or shared session. Never the transaction pooler.
The shared pooler is IPv4 on every plan. The direct and dedicated endpoints are IPv6 by default; IPv4 needs Supabase’s paid IPv4 add-on. That one difference explains most ENOTFOUND reports.
All four Supabase connection modes
| Mode | Host pattern | Port | IPv4 / IPv6 by plan | Username format | Prepared statements | Best for |
|---|---|---|---|---|---|---|
| Direct | db.[ref].supabase.co | 5432 | IPv6 by default; IPv4 add-on | postgres | Yes | Migrations, pg_dump, long-lived IPv6 apps |
| Shared session | aws-[n]-[region].pooler.supabase.com | 5432 | IPv4 on every plan | postgres.[ref] | Yes | Persistent IPv4 apps, BI tools, database GUIs |
| Shared transaction | aws-[n]-[region].pooler.supabase.com | 6543 | IPv4 on every plan | postgres.[ref] | No | Serverless, edge, short-lived connections |
| Dedicated | db.[ref].supabase.co | 6543 | Paid plans; IPv6 by default, IPv4 add-on | postgres | No | High-performance apps on paid plans |
The dedicated pooler shares a hostname with the direct connection and uses port 6543. It supports transaction mode only. A session-mode connection on a paid plan still goes through the shared pooler.
[n] is a pooler cluster index, not part of the region. A region can have more than one. Copy the full host from Dashboard → Connect; do not assemble it from a blog post or from memory.
Which mode should you use?
Match the mode to the lifetime of the connection, not to the framework name.
- Vercel, Cloudflare, Lambda, or another short-lived runtime: shared transaction on
6543. Turn off prepared statements in the driver. - Long-running server on an IPv4-only network: shared session on
5432. It keeps session state and supports prepared statements. - Long-running server with IPv6: direct on
5432. It has the fewest moving parts. - High connection volume on a paid plan: dedicated pooler on
6543. Budget for transaction-mode limits. - Migration or dump: direct or shared session. Transaction pooling cannot hold the session your tool needs.
When in doubt, a shared session string is the safest diagnostic default because it works on IPv4 and accepts prepared statements. Move to transaction mode once you know the deployment is serverless.
Copy the string from Supabase
- Open the Supabase Dashboard for the project.
- Choose Connect.
- Select Direct, Session pooler, Transaction pooler, or Dedicated pooler.
- Copy the complete string.
- Replace only the password placeholder.
postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=requireKeep the host and username exactly as shown. The shared pooler username is postgres.[ref], not postgres. A custom database role follows the same role.[ref] shape on the shared pooler.
Test the string before wiring it into the app:
psql "postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=require" -c "select 1"If that succeeds, the host, port, username, password, and TLS settings are all working. If it fails, the error usually names the layer that broke.
Usernames are the second-most-common break
| Mode | Username | What goes wrong |
|---|---|---|
| Direct | postgres | Adding .[ref] produces an authentication failure. |
| Dedicated | postgres | Same as direct: plain role, no project suffix. |
| Shared session | postgres.[ref] | Dropping .[ref] gives Tenant or user not found. |
| Shared transaction | postgres.[ref] | Dropping .[ref] gives the same tenant error. |
The shared pooler has to find the tenant before it can find the role. Without the project ref, postgres is ambiguous. Supabase returns Tenant or user not found or FATAL: (ENOTFOUND) tenant/user postgres.[ref] not found when the host and username do not belong together.
The fix is not to guess. Copy both values from the same Connect string. If you rotate credentials, use the same role and project ref; the pooler may keep old credentials until existing connections recycle.
Passwords and URL encoding
A connection string is a URL. A raw @ changes where the password ends. A raw # starts a fragment, and a raw / starts the database path. Encode reserved characters before placing a password in the URI:
raw: p@ss word
encoded: p%40ss%20word@becomes%40#becomes%23?becomes%3F/becomes%2F- a space becomes
%20
A password with only letters and numbers can go in unchanged. When in doubt, encode the password component with encodeURIComponent() in JavaScript or urllib.parse.quote() in Python.
If the server says FATAL: password authentication failed, check the username first, then the password. A correctly encoded password for the wrong role still fails. If you reset the password in the Dashboard, update every environment variable that uses it and restart the app so pooled connections reconnect with the new credentials.
SSL: require encrypts, verify-full verifies
Supabase accepts unencrypted connections unless you enable SSL enforcement. Set sslmode=require at minimum. It encrypts traffic, but it does not verify that the server is really Supabase.
To verify the server identity, download the Supabase root certificate from Database settings and use verify-full with sslrootcert:
postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=verify-full&sslrootcert=prod-ca-2021.crtThe certificate filename is prod-ca-2021.crt. Keep it out of source control if your deployment policy treats certificates as configuration. For most applications, sslmode=require is the practical default; verify-full is the stronger choice when you can ship and rotate the CA.
Do not use sslmode=prefer for Supabase. It allows a silent fallback to an unencrypted connection. Do not use sslmode=disable outside a local test.
Driver settings for transaction mode
Transaction pooling hands a server connection back after each transaction. Session state and named prepared statements do not survive. Each driver disables them differently:
| Driver | Setting | Notes |
|---|---|---|
| Prisma | ?pgbouncer=true | Use a direct or session URL for migrations. |
| Drizzle + postgres.js | prepare: false | Set it when creating the client. |
| node-postgres | Omit name | An unnamed query is not a named prepared statement. |
| psycopg 3 | prepare_threshold=None | Disables automatic prepared statements. |
Prisma
DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:6543/postgres?sslmode=require&pgbouncer=true"
DIRECT_URL="postgresql://postgres.[ref]:[password]@aws-[n]-[region].pooler.supabase.com:5432/postgres?sslmode=require"On Prisma 6, put directUrl = env("DIRECT_URL") in the datasource block and use DATABASE_URL for the pooled runtime connection. On Prisma 7 and later, directUrl is deprecated: put the direct or session URL in prisma.config.ts as datasource.url, and create Prisma Client with a driver adapter such as @prisma/adapter-pg.
Prisma 7 recommends leaving out pgbouncer=true on PgBouncer 1.21 or newer. Supabase’s pooler is Supavisor, and Supabase still documents the flag for pooled Prisma connections. Keep it on the shared transaction string; it is the setting Prisma expects for a pooler that does not support prepared statements.
Drizzle with postgres.js
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL!, { prepare: false });node-postgres
await client.query({ text: "select 1" });Leave out name. Supplying a name turns the query into a prepared statement that the next transaction may not find.
psycopg 3
import psycopg
conn = psycopg.connect(conninfo, prepare_threshold=None)Supabase connection errors and what they mean
| Error | Cause | Fix |
|---|---|---|
getaddrinfo ENOTFOUND db.[ref].supabase.co | IPv4-only network hitting the IPv6-only direct host. | Use shared session on 5432, or buy the IPv4 add-on. |
FATAL: (ENOTFOUND) tenant/user postgres.[ref] not found | Pooler host and username are from different modes or projects. | Copy host and username from the same Connect string. |
prepared statement "s0" already exists | Named prepared statements through transaction pooling. | Disable them in the driver, or use direct or shared session. |
FATAL: password authentication failed | Wrong role or password, or the password is not URL-encoded. | Check the username, encode reserved characters, then reset if needed. |
ENETUNREACH / ETIMEDOUT | An IPv6 address was returned but the network has no route or is blocked. | Use the shared pooler or add IPv4. |
Max client connections reached / No more connections allowed (max_client_conn) | The pooler has exhausted its client connection limit. | Use transaction mode, reduce pool size, close clients, or upgrade compute. |
Start with the exact error text. ENOTFOUND is a DNS problem, ETIMEDOUT is a path problem, and password authentication failed is a credential problem. They point at different fixes.
What was tested
On October 23, 2026, both shared pooler modes connected to a 20-table test project and read the schema plus Supabase’s auth and storage tables. Session mode on 5432 and transaction mode on 6543 each completed in 2–4 seconds. Prepared statements were disabled for the transaction-mode run.
The direct endpoint was not tested from an IPv4-only network. Its IPv6 default and the IPv4 add-on behavior come from Supabase’s current connection docs, not from that test run.
Frequently asked questions
Sources
- Supabase: Connecting to your database
- Supabase: Connection pooling and limits
- Supabase: IPv4 and IPv6 compatibility
- Supabase: Postgres SSL enforcement
- Supabase: Disabling prepared statements
- Supabase: Password authentication failed
- Supabase: Tenant or user not found
- Supabase: Too many connections
- Supabase: Connecting with Prisma
- Prisma 7: Configure Prisma Client with PgBouncer
- Drizzle: Connect Supabase
- node-postgres: Prepared statements
- psycopg 3: Prepared statements