Quick start
After creating your MPG cluster and attaching your app, these are the essentials: 1. Your connection string. When you attach an app withfly mpg attach, Fly sets a DATABASE_URL secret on your app automatically. You can customize the variable name during attachment. Your app receives this as an environment variable at runtime. Both pooled and direct URLs are available from the Connect tab in your cluster’s dashboard.
- Pooled URL (default):
postgresql://fly-user:YOUR_PASSWORD@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db— routes through PgBouncer. Use this for your application. - Direct URL:
postgresql://fly-user:YOUR_PASSWORD@direct.YOUR_CLUSTER.flympg.net/fly-db— bypasses PgBouncer. Use this for migrations, advisory locks, orLISTEN/NOTIFY.
sslmode in your connection string.
2. Set connection lifetime and idle timeout in your code. These are settings you configure in your application’s database client or connection pool library — not on the database or cluster side. Not all client libraries support these settings directly — see the language-specific examples below.
3. Set up a direct URL for migrations. Most frameworks run migrations on deploy. Migrations use advisory locks and other session-scoped features that require the direct URL, not the pooled one.
Why client configuration matters
Your Fly.io apps, as well as your Fly.io Postgres databases, sit behind Fly.io’s proxy. Public and private traffic route through this proxy. Sometimes our proxy restarts and the proxy does its best to drain connections before restarting. Postgres doesn’t have a protocol level mechanism to tell clients to stop sending queries on a particular connection, so we have to rely on client-side configuration to handle graceful handoff. The proxy’s shutdown timeout is 10 minutes. Any connection that remains open after that is terminated. If your application holds connections for longer than this — which is the default behavior of most connection pools — you might run into errors liketcp recv (idle): closed or ECONNRESET during proxy deployments.
The fix is straightforward: configure your connection pool to proactively recycle connections on a shorter interval than the proxy’s timeout.
PgBouncer mode and your client
All MPG clusters include PgBouncer for connection pooling. The pool mode you choose on the cluster side affects what your client can do. See Cluster Configuration for how to change modes. Session mode (default): A PgBouncer connection is held for the entire client session. Full PostgreSQL feature compatibility — prepared statements, advisory locks,LISTEN/NOTIFY, and multi-statement transactions all work normally. Lower connection reuse.
Transaction mode: PgBouncer assigns a connection per transaction and returns it to the pool afterward. Higher throughput and connection reuse, but:
- Named prepared statements don’t work — you must use unnamed/extended query protocol
- Advisory locks are not session-scoped — use the direct URL for migrations
LISTEN/NOTIFYdoesn’t work — use an alternative notifier (see the Phoenix guide for Oban examples)SETcommands affect only the current transaction
Language-specific configuration
Node.js — pg (node-postgres)
Node.js — pg (node-postgres)
maxLifetimeSeconds was added in pg-pool 3.5.1 (included with pg 8.8+). If you’re on an older version, upgrade — this setting is critical for reliable connections on Fly.Node.js — Prisma
Node.js — Prisma
Add the following query parameters to your connection string:Prisma manages its own connection pool internally. The
In your Prisma schema:
connection_limit parameter controls the pool size per Prisma client instance.Python — SQLAlchemy
Python — SQLAlchemy
pool_recycle is the max connection lifetime — SQLAlchemy will close and replace connections older than this value.pool_pre_ping issues a lightweight SELECT 1 before each connection checkout. This adds a small round-trip but catches stale connections before your query fails.Python — psycopg3 connection pool
Python — psycopg3 connection pool
Go — database/sql with pgx
Go — database/sql with pgx
database/sql handles connection recycling natively. SetConnMaxLifetime is the key setting — it ensures no connection is reused beyond the specified duration.To disable prepared statements for PgBouncer transaction mode, use the default_query_exec_mode connection parameter:Ruby — ActiveRecord (Rails)
Ruby — ActiveRecord (Rails)
max_age natively:Elixir/Phoenix — Ecto
Elixir/Phoenix — Ecto
Note on connection lifetime in Ecto:
:max_lifetime requires DBConnection 2.10.0 or later (2.10.1+ if your connections sit idle). It takes a range in milliseconds rather than a single value, so reconnects are spread out instead of the whole pool expiring at once. MPG’s PgBouncer closes client connections after 600s idle (client_idle_timeout) and retires its own server connections after 600s (server_lifetime). Recycling on a 9-10 minute schedule keeps your pool ahead of both, so connections are replaced on your terms instead of turning up closed mid-query.Connection limits
Each MPG plan has a fixed number of PgBouncer connection slots shared across all clients. If your total pool size (across all app processes) exceeds this limit, new connections will be queued or rejected.
Max client connections is the total number of client connections PgBouncer will accept. Max database connections is the number of actual connections PgBouncer opens to PostgreSQL. The reserve pool handles bursts above the normal pool size.
Common connection limit errors
FATAL: too many connections for role or remaining connection slots are reserved for roles with the SUPERUSER attribute: Your total pool size across all processes exceeds the PgBouncer connection limit. To fix:
- Reduce
pool_size/maxin each process - Switch to transaction pool mode for better connection reuse
- Check for connection leaks (connections opened but never returned to the pool)
pool_size: 10 and 2 worker processes with pool_size: 5, your total is (3 × 10) + (2 × 5) = 40 connections.
Troubleshooting
tcp recv (idle): closed or tcp recv (idle): timeout
Cause: PgBouncer or the proxy closed an idle connection. MPG’s PgBouncer closes client connections after 600s idle (client_idle_timeout). Proxy deployments also drain connections when old instances shut down.
Fix: Set your client’s idle timeout to 300 seconds (5 min) and max connection lifetime to 600 seconds (10 min), which keeps your pool ahead of PgBouncer’s own idle timeout. Most connection pools reconnect automatically when a connection is closed, so these errors are transient. If you see them often, reduce your pool size so fewer connections sit idle.
ECONNRESET or “connection reset by peer”
Cause: A long-lived connection was terminated by something upstream, such as a proxy restart draining its remaining connections.
Fix: Set max connection lifetime to 600 seconds (10 min) or less, so your pool recycles connections on its own schedule. Enable retry logic with backoff for transient failures.
Prepared statement errors
Errors likeprepared statement "..." does not exist, prepared statement "..." already exists, or protocol_violation on login all point to the same root cause: your client is sending named prepared statements through PgBouncer in transaction mode. PgBouncer assigns a different backend connection per transaction, so prepared statements created on one connection aren’t available on the next.
Fix: Disable named prepared statements in your client configuration:
- Node.js (pg): This is the default behavior — no change needed
- Prisma: Add
?pgbouncer=trueto your connection string - Python (psycopg3): Set
prepare_threshold=None - Go (pgx): Use
default_query_exec_mode=exec - Ruby (ActiveRecord): Set
prepared_statements: false - Elixir (Ecto): Set
prepare: :unnamed
Connection hangs on startup
Cause: DNS resolution failure on Fly’s internal IPv6 network. Your app can’t resolve the.flympg.net address.
Fix: Ensure your app is configured for IPv6. For Elixir apps, see the IPv6 settings guide. For other runtimes, verify that your DNS resolver supports AAAA records on the Fly private network.