Back to Midday

Database Connection Pooling

docs/database-connection-pooling.md

latest4.0 KB
Original Source

Database Connection Pooling

Technical documentation for the database connection setup across Supabase and Railway.

Overview

The application connects to Supabase Postgres through Supavisor (Supabase's shared connection pooler) in transaction mode. Each Railway region's API instance reads from the closest Supabase read replica and writes to the primary database in EU.

Connection Modes

Supabase offers two pooling modes via pooler.supabase.com:

ModePortBehavior
Session mode54321:1 client-to-backend mapping. No real pooling — each app connection holds a dedicated Postgres connection for its entire lifetime.
Transaction mode6543Real connection pooling. Backend connections are shared between clients and only held during a transaction, then returned to the pool.

We use transaction mode (port 6543). This is critical — session mode on port 5432 provides zero pooling benefit despite routing through pooler.supabase.com.

Connection String Format

postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres

Dedicated Pooler (PgBouncer)

Supabase also offers a dedicated PgBouncer co-located on the database machine at db.<ref>.supabase.co:6543. This has lower latency (no network hop to a separate server) but requires IPv6 connectivity or the Supabase IPv4 add-on ($4/mo per database).

Railway does not support IPv6 to Supabase's direct endpoints, so the shared Supavisor pooler is the correct choice for our infrastructure.

Multi-Region Replica Mapping

The API runs in 3 Railway regions. Each instance reads from the closest Supabase read replica via the RAILWAY_REPLICA_REGION environment variable:

Railway RegionEnv VarSupabase RegionRole
europe-west4-drams3aDATABASE_FRA_URLeu-central-1Primary (reads + writes)
us-east4-eqdc4aDATABASE_IAD_URLus-east-1Read replica
us-west2DATABASE_SJC_URLus-west-1Read replica
  • Reads are routed to the regional replica via executeOnReplica and the withReplicas wrapper in packages/db/src/replicas.ts.
  • Writes always go to DATABASE_PRIMARY_URL (the primary in eu-central-1).

Pool Configuration

Defined in packages/db/src/client.ts:

SettingDevelopmentProduction
max840
idleTimeoutMillis5,000ms60,000ms
connectionTimeoutMillis5,000ms5,000ms
maxUses1000 (unlimited)
ssldisabledenabled (rejectUnauthorized: false)

Each API instance creates up to 2 pools (primary + 1 regional replica), so the maximum client connections per instance is 40 × 2 = 80. With Supavisor transaction mode, these are multiplexed into a much smaller number of actual Postgres backend connections. Pool sizes are tuned for PgBouncer's 600 client limit across API + worker instances.

Prepared Statements

We use pg (node-postgres) for all database clients. Transaction mode (port 6543) does not support prepared statements — use session pooler (port 5432) or direct connection for pg compatibility.

See Supabase: Disabling prepared statements.

Environment Variables

API Service

  • DATABASE_PRIMARY_URL — Primary database (EU, writes + reads)
  • DATABASE_FRA_URL — EU read replica (same as primary)
  • DATABASE_IAD_URL — US East read replica
  • DATABASE_SJC_URL — US West read replica

Worker Service

  • DATABASE_PRIMARY_POOLER_URL — Primary database (EU)

Dashboard Service

  • No database variables — connects to the API via tRPC, not directly to Postgres.

Railway Deploy Configuration

  • API: 3 regions × 1 replica each (production), 3 regions × 1 replica each (staging)
  • Dashboard: 3 regions × 2 replicas each (production), 3 regions × 1 replica each (staging, with serverless/sleep enabled)
  • Worker: 1 region (EU) × 3 replicas (production)