From 0cd8652bd1d3cb0ab065c4d1b825aeed87d81575 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 25 Mar 2026 16:11:49 -0500 Subject: [PATCH 01/14] docs: add client configuration guides for MPG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds comprehensive documentation for configuring Managed Postgres client connections, addressing a critical gap where users lacked guidance on preventing dropped connections during proxy maintenance. The core problem: Fly's edge proxy restarts periodically (typically during deployments), and connections held longer than the proxy's 15-minute shutdown timeout are forcibly closed. Without proper configuration, most applications encounter `ECONNRESET` or `tcp recv (idle): closed` errors during these restarts. Connection pool configuration is the fix, but it was poorly documented across the MPG docs. This change adds two guides. "Connect Your Client" is a quick-start checklist covering the essential settings: max connection lifetime (600s), idle timeout (300s), pool size recommendations, and prepared statement handling. "Client-Side Connection Configuration" is a comprehensive reference with language-specific examples for Node.js, Python, Go, Ruby, and Elixir, detailed explanations of how PgBouncer modes affect client behavior, connection limit tables per plan tier, and a full troubleshooting section addressing common errors. The navigation has been restructured to surface both guides prominently, and existing guides (cluster configuration, Phoenix) now cross-link to the new documentation. This makes the recommended connection settings discoverable at the point where users need them most — when connecting their applications to MPG. --- mpg/client-configuration.html.md | 288 ++++++++++++++++++++++ mpg/configuration.html.md | 4 +- mpg/connect-your-client.html.md | 48 ++++ mpg/guides-examples/phoenix-guide.html.md | 4 +- partials/_mpg_nav.html.erb | 10 +- 5 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 mpg/client-configuration.html.md create mode 100644 mpg/connect-your-client.html.md diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md new file mode 100644 index 0000000000..6ebd035750 --- /dev/null +++ b/mpg/client-configuration.html.md @@ -0,0 +1,288 @@ +--- +title: "Client-Side Connection Configuration" +layout: docs +nav: mpg +date: 2026-03-25 +--- + +This guide covers how to configure your application's database client for reliable, performant connections to Fly Managed Postgres. It explains why certain settings matter and provides configuration examples for popular libraries across multiple languages. + +For a quick summary of the essentials, see [Connect Your Client](/docs/mpg/connect-your-client/). + +## Why client configuration matters + +Your application connects to Managed Postgres through Fly's edge proxy and PgBouncer. The proxy handles TLS termination and routes traffic across Fly's internal network. Periodically — typically during proxy deployments — the proxy needs to restart. + +For HTTP/2 and WebSocket connections, the proxy sends a `GOAWAY` frame that tells clients to gracefully finish in-flight requests and open new connections. The PostgreSQL wire protocol has no equivalent mechanism. When the proxy restarts, it waits for existing connections to drain, but it can't tell your Postgres client to stop sending queries on the current connection. + +The proxy's shutdown timeout is **15 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'll see errors like `tcp 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. + +## Recommended settings + +| Setting | Recommended value | Why | +|---------|-------------------|-----| +| Max connection lifetime | **600s** (10 min) | Connections recycle before the proxy's 15-min shutdown timeout | +| Idle connection timeout | **300s** (5 min) | Releases unused connections before they're forcibly closed | +| Pool size | **5–10** (Basic/Starter), **10–20** (Launch+) | Match your plan's PgBouncer capacity | +| Prepared statements | **Disabled** in transaction mode | PgBouncer can't track per-connection prepared statement state | +| Connection retries | **Enabled** with backoff | Handle transient connection drops during proxy restarts | + +## 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 Options](/docs/mpg/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/NOTIFY`** doesn't work — use an alternative notifier (see the [Phoenix guide](/docs/mpg/guides-examples/phoenix-guide/) for Oban examples) +- **`SET` commands** affect only the current transaction + +If your ORM or driver supports it, transaction mode with unnamed prepared statements is the better choice for most web applications. + +## Language-specific configuration + +### Node.js — pg (node-postgres) + +```javascript +const { Pool } = require('pg'); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + + // Pool sizing + max: 10, + + // Connection lifecycle + idleTimeoutMillis: 300_000, // 5 min — close idle connections + maxLifetimeMillis: 600_000, // 10 min — recycle before proxy timeout + connectionTimeoutMillis: 5_000, // 5s — fail fast on connection attempts +}); +``` + +`maxLifetimeMillis` was added in `pg-pool` 3.6.0 (included with `pg` 8.11+). If you're on an older version, upgrade — this setting is critical for reliable connections on Fly. + +### Node.js — Prisma + +Add `pgbouncer=true` and connection pool parameters to your connection string: + +```env +DATABASE_URL="postgresql://fly-user:@pgbouncer..flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30" +``` + +The `pgbouncer=true` parameter tells Prisma to disable prepared statements and adjust its connection handling for PgBouncer compatibility. + +In your Prisma schema: + +```prisma +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +``` + +Prisma manages its own connection pool internally. The `connection_limit` parameter controls the pool size per Prisma client instance. If you run multiple processes, keep total connections within your plan's capacity. + +### Python — SQLAlchemy + +```python +import os +from sqlalchemy import create_engine + +engine = create_engine( + os.environ["DATABASE_URL"], + + # Pool sizing + pool_size=10, + max_overflow=5, + + # Connection lifecycle + pool_recycle=600, # 10 min — recycle connections before proxy timeout + pool_timeout=30, # 30s — wait for available connection + pool_pre_ping=True, # verify connections before use + + # Disable prepared statements for PgBouncer transaction mode + # (uncomment if using transaction mode) + # connect_args={"prepare_threshold": 0}, # for psycopg3 + # connect_args={"options": "-c plan_cache_mode=force_custom_plan"}, # alternative +) +``` + +`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 +import os +from psycopg_pool import ConnectionPool + +pool = ConnectionPool( + conninfo=os.environ["DATABASE_URL"], + + # Pool sizing + min_size=2, + max_size=10, + + # Connection lifecycle + max_lifetime=600, # 10 min — recycle before proxy timeout + max_idle=300, # 5 min — close idle connections + reconnect_timeout=5, + + # Disable prepared statements for PgBouncer transaction mode + # (uncomment if using transaction mode) + # kwargs={"prepare_threshold": 0}, +) +``` + +### Go — database/sql with pgx + +```go +import ( + "database/sql" + "os" + "time" + + _ "github.com/jackc/pgx/v5/stdlib" +) + +// Open the connection pool. +db, err := sql.Open("pgx", os.Getenv("DATABASE_URL")) +if err != nil { + log.Fatal(err) +} + +// Pool sizing +db.SetMaxOpenConns(10) +db.SetMaxIdleConns(5) + +// Connection lifecycle +db.SetConnMaxLifetime(10 * time.Minute) // recycle before proxy timeout +db.SetConnMaxIdleTime(5 * time.Minute) // close idle connections +``` + +Go's `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: + +```go +connStr := os.Getenv("DATABASE_URL") + "?default_query_exec_mode=exec" +db, err := sql.Open("pgx", connStr) +``` + +### Ruby — ActiveRecord (Rails) + +```yaml +# config/database.yml +production: + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + idle_timeout: 300 # 5 min — close idle connections + checkout_timeout: 5 + prepared_statements: false # required for PgBouncer transaction mode +``` + +For connection max lifetime, Rails 7.2+ supports `max_lifetime` natively: + +```yaml +# config/database.yml (Rails 7.2+) +production: + url: <%= ENV["DATABASE_URL"] %> + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + max_lifetime: 600 # 10 min — recycle before proxy timeout + idle_timeout: 300 + checkout_timeout: 5 + prepared_statements: false +``` + +On older Rails versions, connections will still be recycled by the idle timeout if your app has enough traffic. For low-traffic apps on older Rails, consider the [`activerecord-connection_reaper`](https://github.com/mperham/activerecord-connection_reaper) gem or a periodic reconnection task. + +### Elixir/Phoenix — Ecto + +```elixir +# config/runtime.exs +config :my_app, MyApp.Repo, + url: System.fetch_env!("DATABASE_URL"), + pool_size: 8, + queue_target: 5_000, + queue_interval: 5_000, + prepare: :unnamed # required for PgBouncer transaction mode +``` + +For comprehensive Phoenix setup including migrations, Oban configuration, and Ecto-specific troubleshooting, see the [Phoenix with Managed Postgres](/docs/mpg/guides-examples/phoenix-guide/) guide. + +
+**Note on connection lifetime in Ecto:** Postgrex does not currently support a max connection lifetime setting. Connections are recycled only when they encounter errors or are explicitly disconnected. The idle timeout and PgBouncer's own `server_lifetime` setting (default 1800s) provide some protection, but for the most reliable behavior during proxy restarts, a `max_lifetime` option in Postgrex/DBConnection would be ideal. This is a known gap. +
+ + + +## 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. + +| Plan | PgBouncer max client connections | Direct max connections | +|------|--------------------------------|----------------------| +| Basic | _TBD_ | _TBD_ | +| Starter | _TBD_ | _TBD_ | +| Launch | _TBD_ | _TBD_ | +| Scale | _TBD_ | _TBD_ | +| Performance | _TBD_ | _TBD_ | + +### 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` / `max` in each process +- Switch to **transaction** pool mode for better connection reuse +- Check for connection leaks (connections opened but never returned to the pool) + +**Calculating your total pool usage:** If you have 3 web processes with `pool_size: 10` and 2 worker processes with `pool_size: 5`, your total is `(3 × 10) + (2 × 5) = 40` connections. This must be within your plan's PgBouncer limit. + +## Troubleshooting + +### `tcp recv (idle): closed` or `tcp recv (idle): timeout` + +**Cause:** The proxy or PgBouncer closed an idle connection. This happens during proxy deployments (the proxy drains connections on restart) or when PgBouncer's idle timeout is reached. + +**Fix:** Set your client's idle timeout to **300 seconds** (5 min) and max connection lifetime to **600 seconds** (10 min). Most connection pools reconnect automatically when a connection is closed — these errors are transient. If you're seeing them frequently outside of proxy deployments, reduce your pool size so fewer connections sit idle. + +### `ECONNRESET` or "connection reset by peer" + +**Cause:** A long-lived connection was terminated during a proxy restart. The proxy's shutdown timeout is 15 minutes — any connection older than that is forcibly closed. + +**Fix:** Set max connection lifetime to **600 seconds** (10 min) so connections are recycled before the proxy needs to kill them. Enable retry logic with backoff for transient failures. + +### `FATAL 08P01 protocol_violation` on login + +**Cause:** Your client is sending named prepared statements through PgBouncer in transaction mode. PgBouncer can't route prepared statements to the correct backend connection in this mode. + +**Fix:** Disable named prepared statements in your client configuration: +- **Node.js (pg):** This is the default behavior — no change needed +- **Prisma:** Add `?pgbouncer=true` to your connection string +- **Python (psycopg3):** Set `prepare_threshold=0` +- **Go (pgx):** Use `default_query_exec_mode=exec` +- **Ruby (ActiveRecord):** Set `prepared_statements: false` +- **Elixir (Ecto):** Set `prepare: :unnamed` + +### `prepared statement "..." does not exist` + +**Cause:** Same as above — named prepared statements being used with PgBouncer in transaction mode. + +**Fix:** Same as the `protocol_violation` fix above. + +### 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](https://fly.io/docs/elixir/getting-started/#important-ipv6-settings). For other runtimes, verify that your DNS resolver supports AAAA records on the Fly private network. diff --git a/mpg/configuration.html.md b/mpg/configuration.html.md index 157d4d0ea5..317eaa427f 100644 --- a/mpg/configuration.html.md +++ b/mpg/configuration.html.md @@ -13,7 +13,9 @@ date: 2025-08-18 ## Connection Pooling -All Managed Postgres clusters come with PGBouncer for connection pooling, which helps manage database connections efficiently. You can configure how PGBouncer assigns connections to clients by changing the pool mode. +All Managed Postgres clusters come with PGBouncer for connection pooling, which helps manage database connections efficiently. You can configure how PGBouncer assigns connections to clients by changing the pool mode. + +For configuring your application's connection pool settings (lifetime, idle timeout, pool size, and language-specific examples), see [Client-Side Connection Configuration](/docs/mpg/client-configuration/). ### Pool Mode Options diff --git a/mpg/connect-your-client.html.md b/mpg/connect-your-client.html.md new file mode 100644 index 0000000000..404ddde914 --- /dev/null +++ b/mpg/connect-your-client.html.md @@ -0,0 +1,48 @@ +--- +title: Connect Your Client +layout: docs +nav: mpg +date: 2026-03-25 +--- + +After [creating your MPG cluster](/docs/mpg/create-and-connect/) and attaching your app, configure your database client for reliable connections. These settings prevent dropped connections during routine proxy maintenance and keep your connection pool healthy. + +## Use the pooled connection URL + +Always connect your application through PgBouncer using the **pooled URL** (the default when you attach an app). The pooled URL looks like: + +``` +postgresql://fly-user:@pgbouncer..flympg.net/fly-db +``` + +Use the **direct URL** (`direct..flympg.net`) only for migrations, advisory locks, or `LISTEN/NOTIFY` — operations that require session-level stickiness. + +## Set connection lifetime and idle timeout + +Fly's edge proxy mediates connections between your app and your database. During routine deployments, the proxy restarts and long-lived connections are severed. Set your client's **max connection lifetime** so connections are recycled before the proxy needs to kill them. + +| Setting | Recommended value | Why | +|---------|-------------------|-----| +| Max connection lifetime | **600 seconds** (10 min) | Connections recycle before the proxy's 15-min shutdown timeout | +| Idle connection timeout | **300 seconds** (5 min) | Releases unused connections before they're forcibly closed | + +## Keep your pool size modest + +Match your pool size to your plan's capacity. Oversized pools waste PgBouncer slots and can trigger connection limit errors. + +| Plan tier | Suggested pool size per process | +|-----------|-------------------------------| +| Basic / Starter | 5–10 | +| Launch and above | 10–20 | + +If you run multiple processes (e.g., web + background workers), the total across all processes should stay within these ranges. + +## Disable prepared statements in transaction mode + +If your PgBouncer pool mode is set to **Transaction** (required for Ecto; recommended for high-throughput apps), you must disable named prepared statements in your client. PgBouncer can't track prepared statements across transactions. + +See [Cluster Configuration Options](/docs/mpg/configuration/) for how to change your pool mode. + +## Next steps + +For language-specific configuration examples (Node.js, Python, Go, Ruby, Elixir), detailed troubleshooting, and connection limit details, see the full [Client-Side Connection Configuration](/docs/mpg/client-configuration/) guide. diff --git a/mpg/guides-examples/phoenix-guide.html.md b/mpg/guides-examples/phoenix-guide.html.md index 6a12c8cedb..896a4a6ccd 100644 --- a/mpg/guides-examples/phoenix-guide.html.md +++ b/mpg/guides-examples/phoenix-guide.html.md @@ -10,6 +10,8 @@ author: Kaelyn Illustration by Annie Ruygt of a Phoenix bird resting with Frankie the balloon looking on +For general connection configuration that applies to all languages — connection lifetime, idle timeouts, proxy restart behavior, and troubleshooting — see [Client-Side Connection Configuration](/docs/mpg/client-configuration/). + This guide explains the key **Managed Postgres (MPG)-specific adjustments** you need when connecting a Phoenix app. We'll focus on: 1. Connection Pooling Settings @@ -118,7 +120,7 @@ Older versions required the Repeater plugin. Since Oban 2.14 (2023), polling fal ### Common errors and fixes -- `tcp recv (idle): closed` or `tcp recv (idle): timeout` — These are idle connection reclaimed by the pooler, and don't represent an issue as Ecto reconnects automatically. To remove them, lower your pool size or ignore. +- `tcp recv (idle): closed` or `tcp recv (idle): timeout` — These occur when the Fly proxy or PgBouncer closes an idle connection, often during routine proxy deployments. Ecto reconnects automatically, so these are transient. To reduce their frequency, lower your pool size so fewer connections sit idle. For a full explanation of why this happens and how to configure connection lifetime and idle timeouts, see [Client-Side Connection Configuration — Troubleshooting](/docs/mpg/client-configuration/#troubleshooting). - `FATAL 08P01 protocol_violation` on login — Set `prepare: :unnamed` and ensure PgBouncer is in Transaction mode. - Oban jobs not running — Use a non-Postgres notifier (PG or Phoenix) behind PgBouncer, or run Oban on a direct Repo. On Oban ≥ 2.14, do not add Repeater (polling fallback is automatic when PubSub isn't available). - Migrations hanging or failing — Run migrations with the direct database URL (via `release_command` or a one-off SSH command), not through PgBouncer. diff --git a/partials/_mpg_nav.html.erb b/partials/_mpg_nav.html.erb index a3b3ec6331..6634209466 100644 --- a/partials/_mpg_nav.html.erb +++ b/partials/_mpg_nav.html.erb @@ -15,7 +15,15 @@ open: true, links: [ { text: "Create and Connect to a Managed Postgres Cluster", path: "/docs/mpg/create-and-connect/" }, - { text: "Cluster Configuration Options", path: "/docs/mpg/configuration/" } + { text: "Cluster Configuration Options", path: "/docs/mpg/configuration/" }, + { text: "Connect Your Client", path: "/docs/mpg/connect-your-client/" } + ] + }, + { + title: "Connection & Clients", + open: true, + links: [ + { text: "Client-Side Connection Configuration", path: "/docs/mpg/client-configuration/" } ] }, { From d3109c327ef1e34c52bf86f09f1c381e2728d1dd Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 25 Mar 2026 16:18:48 -0500 Subject: [PATCH 02/14] Correct some words and values --- mpg/client-configuration.html.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 6ebd035750..f78e541837 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -11,11 +11,9 @@ For a quick summary of the essentials, see [Connect Your Client](/docs/mpg/conne ## Why client configuration matters -Your application connects to Managed Postgres through Fly's edge proxy and PgBouncer. The proxy handles TLS termination and routes traffic across Fly's internal network. Periodically — typically during proxy deployments — the proxy needs to restart. +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. -For HTTP/2 and WebSocket connections, the proxy sends a `GOAWAY` frame that tells clients to gracefully finish in-flight requests and open new connections. The PostgreSQL wire protocol has no equivalent mechanism. When the proxy restarts, it waits for existing connections to drain, but it can't tell your Postgres client to stop sending queries on the current connection. - -The proxy's shutdown timeout is **15 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'll see errors like `tcp recv (idle): closed` or `ECONNRESET` during proxy deployments. +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 like `tcp 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. @@ -25,7 +23,7 @@ The fix is straightforward: configure your connection pool to **proactively recy |---------|-------------------|-----| | Max connection lifetime | **600s** (10 min) | Connections recycle before the proxy's 15-min shutdown timeout | | Idle connection timeout | **300s** (5 min) | Releases unused connections before they're forcibly closed | -| Pool size | **5–10** (Basic/Starter), **10–20** (Launch+) | Match your plan's PgBouncer capacity | +| Pool size | **5–10** | Match your plan's PgBouncer capacity (see Connection tab of your cluster's dashboard) | | Prepared statements | **Disabled** in transaction mode | PgBouncer can't track per-connection prepared statement state | | Connection retries | **Enabled** with backoff | Handle transient connection drops during proxy restarts | From f7811bf8e814c223afa764ae564bd9f9f210d988 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 25 Mar 2026 16:23:47 -0500 Subject: [PATCH 03/14] docs: wrap language examples in collapsible details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client configuration guide lists examples for seven different languages and drivers. This creates a long, dense page that's difficult to scan. Wrapping each language section in `
` tags makes the page more discoverable—users can quickly find their language of choice and expand only what they need. This also improves the reading experience for users who are only interested in one or two specific languages, reducing visual clutter while keeping all reference material readily accessible. --- mpg/client-configuration.html.md | 35 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index f78e541837..da0cda444a 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -44,7 +44,8 @@ If your ORM or driver supports it, transaction mode with unnamed prepared statem ## Language-specific configuration -### Node.js — pg (node-postgres) +
+Node.js — pg (node-postgres) ```javascript const { Pool } = require('pg'); @@ -64,7 +65,10 @@ const pool = new Pool({ `maxLifetimeMillis` was added in `pg-pool` 3.6.0 (included with `pg` 8.11+). If you're on an older version, upgrade — this setting is critical for reliable connections on Fly. -### Node.js — Prisma +
+ +
+Node.js — Prisma Add `pgbouncer=true` and connection pool parameters to your connection string: @@ -85,7 +89,10 @@ datasource db { Prisma manages its own connection pool internally. The `connection_limit` parameter controls the pool size per Prisma client instance. If you run multiple processes, keep total connections within your plan's capacity. -### Python — SQLAlchemy +
+ +
+Python — SQLAlchemy ```python import os @@ -114,7 +121,10 @@ engine = create_engine( `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 ```python import os @@ -138,7 +148,10 @@ pool = ConnectionPool( ) ``` -### Go — database/sql with pgx +
+ +
+Go — database/sql with pgx ```go import ( @@ -173,7 +186,10 @@ connStr := os.Getenv("DATABASE_URL") + "?default_query_exec_mode=exec" db, err := sql.Open("pgx", connStr) ``` -### Ruby — ActiveRecord (Rails) +
+ +
+Ruby — ActiveRecord (Rails) ```yaml # config/database.yml @@ -200,7 +216,10 @@ production: On older Rails versions, connections will still be recycled by the idle timeout if your app has enough traffic. For low-traffic apps on older Rails, consider the [`activerecord-connection_reaper`](https://github.com/mperham/activerecord-connection_reaper) gem or a periodic reconnection task. -### Elixir/Phoenix — Ecto +
+ +
+Elixir/Phoenix — Ecto ```elixir # config/runtime.exs @@ -220,6 +239,8 @@ For comprehensive Phoenix setup including migrations, Oban configuration, and Ec +
+ ## 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. - -| Plan | PgBouncer max client connections | Direct max connections | -|------|--------------------------------|----------------------| -| Basic | _TBD_ | _TBD_ | -| Starter | _TBD_ | _TBD_ | -| Launch | _TBD_ | _TBD_ | -| Scale | _TBD_ | _TBD_ | -| Performance | _TBD_ | _TBD_ | - -### Common connection limit errors +### Common connection 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: @@ -266,7 +248,7 @@ Each MPG plan has a fixed number of PgBouncer connection slots shared across all - Switch to **transaction** pool mode for better connection reuse - Check for connection leaks (connections opened but never returned to the pool) -**Calculating your total pool usage:** If you have 3 web processes with `pool_size: 10` and 2 worker processes with `pool_size: 5`, your total is `(3 × 10) + (2 × 5) = 40` connections. This must be within your plan's PgBouncer limit. +**Calculating your total pool usage:** If you have 3 web processes with `pool_size: 10` and 2 worker processes with `pool_size: 5`, your total is `(3 × 10) + (2 × 5) = 40` connections. ## Troubleshooting @@ -278,9 +260,9 @@ Each MPG plan has a fixed number of PgBouncer connection slots shared across all ### `ECONNRESET` or "connection reset by peer" -**Cause:** A long-lived connection was terminated during a proxy restart. Connections that remain open too long during a proxy drain are forcibly closed. +**Cause:** A long-lived connection was terminated during something like a proxy restart. Connections that remain open too long during a proxy drain may be forcibly closed. -**Fix:** Set max connection lifetime to **600 seconds** (10 min) so connections are recycled before the proxy needs to kill them. Enable retry logic with backoff for transient failures. +**Fix:** Set max connection lifetime to **600 seconds** (10 min) or less so connections are recycled before the proxy needs to kill them. Enable retry logic with backoff for transient failures. ### `FATAL 08P01 protocol_violation` on login From b8c3e855ab8af55526d54df5168814ed58fff25e Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Tue, 14 Apr 2026 10:03:48 -0500 Subject: [PATCH 06/14] fix: remove unverified pool sizes, clean up troubleshooting (#2371) - Remove pool size table and recommended values until actual PgBouncer limits per plan are confirmed - Remove unexplained queue_target/queue_interval from Ecto example - Merge duplicate prepared statement troubleshooting entries into a single section with clearer explanation Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 12 ++---------- mpg/connect-your-client.html.md | 11 ----------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 756a4b4308..55377b7b62 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -225,8 +225,6 @@ On older Rails versions, connections will still be recycled by the idle timeout config :my_app, MyApp.Repo, url: System.fetch_env!("DATABASE_URL"), pool_size: 8, - queue_target: 5_000, - queue_interval: 5_000, prepare: :unnamed # required for PgBouncer transaction mode ``` @@ -264,9 +262,9 @@ For comprehensive Phoenix setup including migrations, Oban configuration, and Ec **Fix:** Set max connection lifetime to **600 seconds** (10 min) or less so connections are recycled before the proxy needs to kill them. Enable retry logic with backoff for transient failures. -### `FATAL 08P01 protocol_violation` on login +### Prepared statement errors -**Cause:** Your client is sending named prepared statements through PgBouncer in transaction mode. PgBouncer can't route prepared statements to the correct backend connection in this mode. +Errors like `prepared 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 @@ -276,12 +274,6 @@ For comprehensive Phoenix setup including migrations, Oban configuration, and Ec - **Ruby (ActiveRecord):** Set `prepared_statements: false` - **Elixir (Ecto):** Set `prepare: :unnamed` -### `prepared statement "..." does not exist` - -**Cause:** Same as above — named prepared statements being used with PgBouncer in transaction mode. - -**Fix:** Same as the `protocol_violation` fix above. - ### Connection hangs on startup **Cause:** DNS resolution failure on Fly's internal IPv6 network. Your app can't resolve the `.flympg.net` address. diff --git a/mpg/connect-your-client.html.md b/mpg/connect-your-client.html.md index 66189a6cee..954ae3ffb4 100644 --- a/mpg/connect-your-client.html.md +++ b/mpg/connect-your-client.html.md @@ -26,17 +26,6 @@ Fly's edge proxy mediates connections between your app and your database. During | Max connection lifetime | **600 seconds** (10 min) | Recycle connections before the proxy closes them | | Idle connection timeout | **300 seconds** (5 min) | Releases unused connections before they're forcibly closed | -## Keep your pool size modest - -Match your pool size to your plan's capacity. Oversized pools waste PgBouncer slots and can trigger connection limit errors. - -| Plan tier | Suggested pool size per process | -|-----------|-------------------------------| -| Basic / Starter | 5–10 | -| Launch and above | 10–20 | - -If you run multiple processes (e.g., web + background workers), the total across all processes should stay within these ranges. - ## Disable prepared statements in transaction mode If your PgBouncer pool mode is set to **Transaction** (required for Ecto; recommended for high-throughput apps), you must disable named prepared statements in your client. PgBouncer can't track prepared statements across transactions. From 08ffdd3b6d1bd80d5a7e50170ad1402e35d68af7 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Tue, 14 Apr 2026 10:26:21 -0500 Subject: [PATCH 07/14] docs: add connection limits, SSL note, direct URL migration guide (#2371) - Fill in PgBouncer connection limits table with actual values per plan - Add SSL note (enabled by default, no sslmode needed) - Expand connect-your-client with DATABASE_URL setup context - Add direct URL migration setup with fly.toml example Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 20 +++++++++++++++++++- mpg/connect-your-client.html.md | 30 ++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 55377b7b62..13d874dd30 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -17,6 +17,10 @@ The proxy's shutdown timeout is **10 minutes**. Any connection that remains open The fix is straightforward: configure your connection pool to **proactively recycle connections** on a shorter interval than the proxy's timeout. +## SSL + +SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string or configure certificates — connections are encrypted automatically. + ## Recommended settings | Setting | Recommended value | Why | @@ -238,7 +242,21 @@ For comprehensive Phoenix setup including migrations, Oban configuration, and Ec
-### Common connection errors +## 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. + +| Plan | Max client connections | Max database connections | Reserve pool | +|------|----------------------|------------------------|-------------| +| Basic | 200 | 50 | 10 | +| Starter | 200 | 50 | 10 | +| Launch | 500 | 150 | 30 | +| Scale | 1000 | 300 | 50 | +| Performance | 1000 | 300 | 50 | + +**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: diff --git a/mpg/connect-your-client.html.md b/mpg/connect-your-client.html.md index 954ae3ffb4..47cda731ce 100644 --- a/mpg/connect-your-client.html.md +++ b/mpg/connect-your-client.html.md @@ -7,15 +7,37 @@ date: 2026-03-25 After [creating your MPG cluster](/docs/mpg/create-and-connect/) and attaching your app, configure your database client for reliable connections. These settings prevent dropped connections during routine proxy maintenance and keep your connection pool healthy. -## Use the pooled connection URL +## Your connection string -Always connect your application through PgBouncer using the **pooled URL** (the default when you attach an app). The pooled URL looks like: +When you attach an app with `fly 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. +MPG provides two connection URLs: + +- **Pooled URL** (default): `postgresql://fly-user:@pgbouncer..flympg.net/fly-db` — routes through PgBouncer. Use this for your application. +- **Direct URL**: `postgresql://fly-user:@direct..flympg.net/fly-db` — bypasses PgBouncer. Use this for migrations, advisory locks, or `LISTEN/NOTIFY`. + +Both URLs are available from the **Connect** tab in your cluster's dashboard. + +SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string. + +## 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. Set both URLs as secrets: + +```bash +fly secrets set \ + DATABASE_URL="postgresql://...@pgbouncer..flympg.net/fly-db" \ + DIRECT_DATABASE_URL="postgresql://...@direct..flympg.net/fly-db" ``` -postgresql://fly-user:@pgbouncer..flympg.net/fly-db + +Then configure your deploy to use the direct URL for migrations. For example, in `fly.toml`: + +```toml +[deploy] + release_command = "/bin/sh -lc 'DATABASE_URL=$DIRECT_DATABASE_URL bin/migrate'" ``` -Use the **direct URL** (`direct..flympg.net`) only for migrations, advisory locks, or `LISTEN/NOTIFY` — operations that require session-level stickiness. +See the [Phoenix guide](/docs/mpg/guides-examples/phoenix-guide/) for Elixir-specific migration setup. ## Set connection lifetime and idle timeout From f29d32b5c9272744cfd6fa0fbb32a6608caf8119 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Tue, 14 Apr 2026 16:26:22 -0500 Subject: [PATCH 08/14] docs: consolidate client connection pages, rename cluster config (#2371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename configuration → cluster-configuration to distinguish cluster-side settings from client-side settings - Merge connect-your-client into client-configuration with a Quick Start section up top and detailed reference below - Delete connect-your-client (now redundant) - Update nav: collapse "Connection & Clients" into "Getting Started", fix all cross-references Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 57 ++++++++++++------ ....html.md => cluster-configuration.html.md} | 2 +- mpg/connect-your-client.html.md | 59 ------------------- mpg/guides-examples/phoenix-guide.html.md | 2 +- partials/_mpg_nav.html.erb | 11 +--- 5 files changed, 44 insertions(+), 87 deletions(-) rename mpg/{configuration.html.md => cluster-configuration.html.md} (98%) delete mode 100644 mpg/connect-your-client.html.md diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 13d874dd30..0d37a8dd85 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -1,38 +1,61 @@ --- -title: "Client-Side Connection Configuration" +title: "Connect Your Client" layout: docs nav: mpg date: 2026-03-25 --- -This guide covers how to configure your application's database client for reliable, performant connections to Fly Managed Postgres. It explains why certain settings matter and provides configuration examples for popular libraries across multiple languages. +This guide covers how to connect your application to Fly Managed Postgres and configure your database client for reliable, performant connections. -For a quick summary of the essentials, see [Connect Your Client](/docs/mpg/connect-your-client/). +## Quick start -## 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. +After [creating your MPG cluster](/docs/mpg/create-and-connect/) and attaching your app, these are the essentials: -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 like `tcp 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. +**1. Your connection string.** When you attach an app with `fly 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. -## SSL +- **Pooled URL** (default): `postgresql://fly-user:@pgbouncer..flympg.net/fly-db` — routes through PgBouncer. Use this for your application. +- **Direct URL**: `postgresql://fly-user:@direct..flympg.net/fly-db` — bypasses PgBouncer. Use this for migrations, advisory locks, or `LISTEN/NOTIFY`. -SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string or configure certificates — connections are encrypted automatically. +SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string. -## Recommended settings +**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](#language-specific-configuration) below. | Setting | Recommended value | Why | |---------|-------------------|-----| -| Max connection lifetime | **600s** (10 min) | Recycle connections before the proxy closes them | -| Idle connection timeout | **300s** (5 min) | Releases unused connections before they're forcibly closed | -| Prepared statements | **Disabled** in transaction mode | PgBouncer can't track per-connection prepared statement state | -| Connection retries | **Enabled** with backoff | Handle transient connection drops during proxy restarts | +| Max connection lifetime | **600 seconds** (10 min) | Recycle connections before the proxy closes them | +| Idle connection timeout | **300 seconds** (5 min) | Releases unused connections before they're forcibly closed | + +**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. + +```bash +fly secrets set \ + DATABASE_URL="postgresql://...@pgbouncer..flympg.net/fly-db" \ + DIRECT_DATABASE_URL="postgresql://...@direct..flympg.net/fly-db" +``` + +```toml +# fly.toml +[deploy] + release_command = "/bin/sh -lc 'DATABASE_URL=$DIRECT_DATABASE_URL bin/migrate'" +``` + +See the [Phoenix guide](/docs/mpg/guides-examples/phoenix-guide/) for Elixir-specific migration setup. + +**4. Disable prepared statements in transaction mode.** If your PgBouncer pool mode is set to **Transaction** (required for Ecto; recommended for high-throughput apps), you must disable named prepared statements in your client. PgBouncer can't track prepared statements across transactions. See [Cluster Configuration](/docs/mpg/cluster-configuration/) for how to change your pool mode. + +--- + +## 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 like `tcp 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 Options](/docs/mpg/configuration/) for how to change modes. +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](/docs/mpg/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. diff --git a/mpg/configuration.html.md b/mpg/cluster-configuration.html.md similarity index 98% rename from mpg/configuration.html.md rename to mpg/cluster-configuration.html.md index 317eaa427f..954b10a27e 100644 --- a/mpg/configuration.html.md +++ b/mpg/cluster-configuration.html.md @@ -15,7 +15,7 @@ date: 2025-08-18 All Managed Postgres clusters come with PGBouncer for connection pooling, which helps manage database connections efficiently. You can configure how PGBouncer assigns connections to clients by changing the pool mode. -For configuring your application's connection pool settings (lifetime, idle timeout, pool size, and language-specific examples), see [Client-Side Connection Configuration](/docs/mpg/client-configuration/). +For configuring your application's connection pool settings (lifetime, idle timeout, pool size, and language-specific examples), see [Connect Your Client](/docs/mpg/client-configuration/). ### Pool Mode Options diff --git a/mpg/connect-your-client.html.md b/mpg/connect-your-client.html.md deleted file mode 100644 index 47cda731ce..0000000000 --- a/mpg/connect-your-client.html.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Connect Your Client -layout: docs -nav: mpg -date: 2026-03-25 ---- - -After [creating your MPG cluster](/docs/mpg/create-and-connect/) and attaching your app, configure your database client for reliable connections. These settings prevent dropped connections during routine proxy maintenance and keep your connection pool healthy. - -## Your connection string - -When you attach an app with `fly 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. - -MPG provides two connection URLs: - -- **Pooled URL** (default): `postgresql://fly-user:@pgbouncer..flympg.net/fly-db` — routes through PgBouncer. Use this for your application. -- **Direct URL**: `postgresql://fly-user:@direct..flympg.net/fly-db` — bypasses PgBouncer. Use this for migrations, advisory locks, or `LISTEN/NOTIFY`. - -Both URLs are available from the **Connect** tab in your cluster's dashboard. - -SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string. - -## 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. Set both URLs as secrets: - -```bash -fly secrets set \ - DATABASE_URL="postgresql://...@pgbouncer..flympg.net/fly-db" \ - DIRECT_DATABASE_URL="postgresql://...@direct..flympg.net/fly-db" -``` - -Then configure your deploy to use the direct URL for migrations. For example, in `fly.toml`: - -```toml -[deploy] - release_command = "/bin/sh -lc 'DATABASE_URL=$DIRECT_DATABASE_URL bin/migrate'" -``` - -See the [Phoenix guide](/docs/mpg/guides-examples/phoenix-guide/) for Elixir-specific migration setup. - -## Set connection lifetime and idle timeout - -Fly's edge proxy mediates connections between your app and your database. During routine deployments, the proxy restarts and long-lived connections are severed. Set your client's **max connection lifetime** so connections are recycled before the proxy needs to kill them. - -| Setting | Recommended value | Why | -|---------|-------------------|-----| -| Max connection lifetime | **600 seconds** (10 min) | Recycle connections before the proxy closes them | -| Idle connection timeout | **300 seconds** (5 min) | Releases unused connections before they're forcibly closed | - -## Disable prepared statements in transaction mode - -If your PgBouncer pool mode is set to **Transaction** (required for Ecto; recommended for high-throughput apps), you must disable named prepared statements in your client. PgBouncer can't track prepared statements across transactions. - -See [Cluster Configuration Options](/docs/mpg/configuration/) for how to change your pool mode. - -## Next steps - -For language-specific configuration examples (Node.js, Python, Go, Ruby, Elixir), detailed troubleshooting, and connection limit details, see the full [Client-Side Connection Configuration](/docs/mpg/client-configuration/) guide. diff --git a/mpg/guides-examples/phoenix-guide.html.md b/mpg/guides-examples/phoenix-guide.html.md index 896a4a6ccd..0b8123c7df 100644 --- a/mpg/guides-examples/phoenix-guide.html.md +++ b/mpg/guides-examples/phoenix-guide.html.md @@ -10,7 +10,7 @@ author: Kaelyn Illustration by Annie Ruygt of a Phoenix bird resting with Frankie the balloon looking on -For general connection configuration that applies to all languages — connection lifetime, idle timeouts, proxy restart behavior, and troubleshooting — see [Client-Side Connection Configuration](/docs/mpg/client-configuration/). +For general connection configuration that applies to all languages — connection lifetime, idle timeouts, proxy restart behavior, and troubleshooting — see [Connect Your Client](/docs/mpg/client-configuration/). This guide explains the key **Managed Postgres (MPG)-specific adjustments** you need when connecting a Phoenix app. We'll focus on: diff --git a/partials/_mpg_nav.html.erb b/partials/_mpg_nav.html.erb index 6634209466..dcba7a0c5a 100644 --- a/partials/_mpg_nav.html.erb +++ b/partials/_mpg_nav.html.erb @@ -15,15 +15,8 @@ open: true, links: [ { text: "Create and Connect to a Managed Postgres Cluster", path: "/docs/mpg/create-and-connect/" }, - { text: "Cluster Configuration Options", path: "/docs/mpg/configuration/" }, - { text: "Connect Your Client", path: "/docs/mpg/connect-your-client/" } - ] - }, - { - title: "Connection & Clients", - open: true, - links: [ - { text: "Client-Side Connection Configuration", path: "/docs/mpg/client-configuration/" } + { text: "Connect Your Client", path: "/docs/mpg/client-configuration/" }, + { text: "Cluster Configuration", path: "/docs/mpg/cluster-configuration/" } ] }, { From 756e8557d1b3112d5f6487c7ec535024cd18b262 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 14:07:37 -0500 Subject: [PATCH 09/14] fix: correct library-specific configuration values (#2371) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node.js: maxLifetimeMillis → maxLifetimeSeconds (value 600, not 600_000), version corrected to pg-pool 3.5.1 / pg 8.8+ - Python: prepare_threshold=0 → None (0 means "prepare on first execution", None actually disables) - Ruby: max_lifetime → max_age, Rails 7.2 → Rails 8.1, remove reference to nonexistent activerecord-connection_reaper gem - Elixir: PgBouncer server_lifetime default is 3600s, not 1800s Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 0d37a8dd85..b45459783d 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -83,13 +83,13 @@ const pool = new Pool({ max: 10, // Connection lifecycle - idleTimeoutMillis: 300_000, // 5 min — close idle connections - maxLifetimeMillis: 600_000, // 10 min — recycle before proxy timeout - connectionTimeoutMillis: 5_000, // 5s — fail fast on connection attempts + idleTimeoutMillis: 300_000, // 5 min — close idle connections + maxLifetimeSeconds: 600, // 10 min — recycle before proxy timeout + connectionTimeoutMillis: 5_000, // 5s — fail fast on connection attempts }); ``` -`maxLifetimeMillis` was added in `pg-pool` 3.6.0 (included with `pg` 8.11+). If you're on an older version, upgrade — this setting is critical for reliable connections on Fly. +`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. @@ -138,7 +138,7 @@ engine = create_engine( # Disable prepared statements for PgBouncer transaction mode # (uncomment if using transaction mode) - # connect_args={"prepare_threshold": 0}, # for psycopg3 + # connect_args={"prepare_threshold": None}, # for psycopg3 # connect_args={"options": "-c plan_cache_mode=force_custom_plan"}, # alternative ) ``` @@ -170,7 +170,7 @@ pool = ConnectionPool( # Disable prepared statements for PgBouncer transaction mode # (uncomment if using transaction mode) - # kwargs={"prepare_threshold": 0}, + # kwargs={"prepare_threshold": None}, ) ``` @@ -227,20 +227,20 @@ production: prepared_statements: false # required for PgBouncer transaction mode ``` -For connection max lifetime, Rails 7.2+ supports `max_lifetime` natively: +For connection max lifetime, Rails 8.1+ supports `max_age` natively: ```yaml -# config/database.yml (Rails 7.2+) +# config/database.yml (Rails 8.1+) production: url: <%= ENV["DATABASE_URL"] %> pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> - max_lifetime: 600 # 10 min — recycle before proxy timeout + max_age: 600 # 10 min — recycle before proxy timeout idle_timeout: 300 checkout_timeout: 5 prepared_statements: false ``` -On older Rails versions, connections will still be recycled by the idle timeout if your app has enough traffic. For low-traffic apps on older Rails, consider the [`activerecord-connection_reaper`](https://github.com/mperham/activerecord-connection_reaper) gem or a periodic reconnection task. +On older Rails versions, there is no built-in max connection age. Connections will still be recycled by the idle timeout if your app has enough traffic, but long-lived busy connections won't be recycled until they encounter an error. @@ -258,7 +258,7 @@ config :my_app, MyApp.Repo, For comprehensive Phoenix setup including migrations, Oban configuration, and Ecto-specific troubleshooting, see the [Phoenix with Managed Postgres](/docs/mpg/guides-examples/phoenix-guide/) guide.
-**Note on connection lifetime in Ecto:** Postgrex does not currently support a max connection lifetime setting. Connections are recycled only when they encounter errors or are explicitly disconnected. The idle timeout and PgBouncer's own `server_lifetime` setting (default 1800s) provide some protection, but for the most reliable behavior during proxy restarts, a `max_lifetime` option in Postgrex/DBConnection would be ideal. This is a known gap. +**Note on connection lifetime in Ecto:** Postgrex does not currently support a max connection lifetime setting. Connections are recycled only when they encounter errors or are explicitly disconnected. The idle timeout and PgBouncer's own `server_lifetime` setting (default 3600s) provide some protection, but for the most reliable behavior during proxy restarts, a `max_lifetime` option in Postgrex/DBConnection would be ideal. This is a known gap.
@@ -310,7 +310,7 @@ Errors like `prepared statement "..." does not exist`, `prepared statement "..." **Fix:** Disable named prepared statements in your client configuration: - **Node.js (pg):** This is the default behavior — no change needed - **Prisma:** Add `?pgbouncer=true` to your connection string -- **Python (psycopg3):** Set `prepare_threshold=0` +- **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` From 70fd5f29686102d2734029a77482bde79b9376d4 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 14:52:24 -0500 Subject: [PATCH 10/14] fix: remove ERB tags from Ruby code examples (#2371) The .html.md files are ERB-processed, so <%= %> in code blocks gets evaluated and then HTML-escaped, rendering as <%= instead of literal ERB syntax. Drop the ERB delimiters since this is just showing the config pattern. Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index b45459783d..bf817529a5 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -220,8 +220,8 @@ db, err := sql.Open("pgx", connStr) ```yaml # config/database.yml production: - url: <%= ENV["DATABASE_URL"] %> - pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + url: ENV["DATABASE_URL"] + pool: ENV.fetch("RAILS_MAX_THREADS", 5) idle_timeout: 300 # 5 min — close idle connections checkout_timeout: 5 prepared_statements: false # required for PgBouncer transaction mode @@ -232,8 +232,8 @@ For connection max lifetime, Rails 8.1+ supports `max_age` natively: ```yaml # config/database.yml (Rails 8.1+) production: - url: <%= ENV["DATABASE_URL"] %> - pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %> + url: ENV["DATABASE_URL"] + pool: ENV.fetch("RAILS_MAX_THREADS", 5) max_age: 600 # 10 min — recycle before proxy timeout idle_timeout: 300 checkout_timeout: 5 From 80ccb8e0907de3044bef0cafb4a2f7c835c9e58b Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 15:00:54 -0500 Subject: [PATCH 11/14] fix: replace angle-bracket placeholders to prevent HTML escaping (#2371) and in code blocks get HTML-escaped by the ERB/Sitepress pipeline, rendering as <password>. Replace with YOUR_PASSWORD and YOUR_CLUSTER. Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 10 +++++----- mpg/guides-examples/phoenix-guide.html.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index bf817529a5..446ff77a1b 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -13,8 +13,8 @@ After [creating your MPG cluster](/docs/mpg/create-and-connect/) and attaching y **1. Your connection string.** When you attach an app with `fly 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:@pgbouncer..flympg.net/fly-db` — routes through PgBouncer. Use this for your application. -- **Direct URL**: `postgresql://fly-user:@direct..flympg.net/fly-db` — bypasses PgBouncer. Use this for migrations, advisory locks, or `LISTEN/NOTIFY`. +- **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, or `LISTEN/NOTIFY`. SSL is enabled by default on all MPG connections. You do not need to set `sslmode` in your connection string. @@ -29,8 +29,8 @@ SSL is enabled by default on all MPG connections. You do not need to set `sslmod ```bash fly secrets set \ - DATABASE_URL="postgresql://...@pgbouncer..flympg.net/fly-db" \ - DIRECT_DATABASE_URL="postgresql://...@direct..flympg.net/fly-db" + DATABASE_URL="postgresql://...@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db" \ + DIRECT_DATABASE_URL="postgresql://...@direct.YOUR_CLUSTER.flympg.net/fly-db" ``` ```toml @@ -99,7 +99,7 @@ const pool = new Pool({ Add `pgbouncer=true` and connection pool parameters to your connection string: ```env -DATABASE_URL="postgresql://fly-user:@pgbouncer..flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30" +DATABASE_URL="postgresql://fly-user:YOUR_PASSWORD@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30" ``` The `pgbouncer=true` parameter tells Prisma to disable prepared statements and adjust its connection handling for PgBouncer compatibility. diff --git a/mpg/guides-examples/phoenix-guide.html.md b/mpg/guides-examples/phoenix-guide.html.md index 0b8123c7df..afdf97299d 100644 --- a/mpg/guides-examples/phoenix-guide.html.md +++ b/mpg/guides-examples/phoenix-guide.html.md @@ -51,8 +51,8 @@ Update your secrets to add a `DIRECT_DATABASE_URL` ```bash fly secrets set \ - DATABASE_URL="postgresql://...@pgbouncer..flympg.net/fly-db" \ - DIRECT_DATABASE_URL="postgresql://...@direct..flympg.net/fly-db" + DATABASE_URL="postgresql://...@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db" \ + DIRECT_DATABASE_URL="postgresql://...@direct.YOUR_CLUSTER.flympg.net/fly-db" ``` In your fly.toml, update your release command to use the direct connection for running your migration: From ec26ca3ed630b40e61e178c3bcd8c714556abf0a Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 15:02:09 -0500 Subject: [PATCH 12/14] fix: drop env language hint on Prisma code fence (#2371) The env syntax hint causes ampersands in the connection string to render as & entities. Plain code fence avoids this. Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 446ff77a1b..e6f8ecfc8a 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -98,7 +98,7 @@ const pool = new Pool({ Add `pgbouncer=true` and connection pool parameters to your connection string: -```env +``` DATABASE_URL="postgresql://fly-user:YOUR_PASSWORD@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30" ``` From 21041c758e155f2e2fad330aadb6122fc1a5d0c9 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 15:21:53 -0500 Subject: [PATCH 13/14] fix: rewrite Prisma example to avoid ampersand escaping (#2371) Ampersands inside
blocks get HTML-entity-escaped regardless of code fences. Replace the inline connection string with a parameter table and a single inline-code URL. Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index e6f8ecfc8a..9239d766c2 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -96,13 +96,17 @@ const pool = new Pool({
Node.js — Prisma -Add `pgbouncer=true` and connection pool parameters to your connection string: +Add the following query parameters to your connection string: -``` -DATABASE_URL="postgresql://fly-user:YOUR_PASSWORD@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30" -``` +| Parameter | Value | Purpose | +|-----------|-------|---------| +| `pgbouncer` | `true` | Disables prepared statements for PgBouncer compatibility | +| `connection_limit` | `10` | Pool size per Prisma client instance | +| `pool_timeout` | `30` | Seconds to wait for a connection | + +Your `DATABASE_URL` should look like: -The `pgbouncer=true` parameter tells Prisma to disable prepared statements and adjust its connection handling for PgBouncer compatibility. +`postgresql://...@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30` In your Prisma schema: From a178a7a8fe83011e44e981dc60b67d032e08eb98 Mon Sep 17 00:00:00 2001 From: Jon Phenow Date: Wed, 15 Apr 2026 15:31:59 -0500 Subject: [PATCH 14/14] fix: remove inline URL that still shows escaped ampersands (#2371) Co-Authored-By: Claude Opus 4.6 (1M context) --- mpg/client-configuration.html.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mpg/client-configuration.html.md b/mpg/client-configuration.html.md index 9239d766c2..3516852e2b 100644 --- a/mpg/client-configuration.html.md +++ b/mpg/client-configuration.html.md @@ -104,10 +104,6 @@ Add the following query parameters to your connection string: | `connection_limit` | `10` | Pool size per Prisma client instance | | `pool_timeout` | `30` | Seconds to wait for a connection | -Your `DATABASE_URL` should look like: - -`postgresql://...@pgbouncer.YOUR_CLUSTER.flympg.net/fly-db?pgbouncer=true&connection_limit=10&pool_timeout=30` - In your Prisma schema: ```prisma