Manage PostgreSQL Connections for Django on Serverless Platforms

Avoid connection exhaustion when Django runs on Lambda, Cloud Run, Vercel, or autoscaling containers: use pooled endpoints (RDS Proxy, Neon, Supabase pooler, PgBouncer), set CONN_MAX_AGE and server-side cursor options correctly, and cap concurrency per instance.

Problem statement

On a traditional server, Django opens a fixed number of database connections: workers times threads. On serverless and fast-autoscaling platforms, each new instance opens its own connections, and a traffic spike can create hundreds of instances in seconds. PostgreSQL's max_connections (often 100 to a few hundred) is exhausted, and requests fail with too many connections. Idle instances may also hold connections open long after traffic drops.

Serverless connection stormTraffic spikemany instancesEach instanceown DB connectionPostgres limitmax_connectionsPooler fixesmultiplex clientsA transaction pooler lets many short-lived instances share a few real connections.
  1. Put a transaction-mode pooler between Django and PostgreSQL: RDS Proxy, PgBouncer, Neon or Supabase pooled endpoints, Cloud SQL with a pooler.
  2. With transaction pooling, set DISABLE_SERVER_SIDE_CURSORS = True and avoid session state (SET without LOCAL, advisory locks across transactions).
  3. Use CONN_MAX_AGE = 0 on functions that freeze between invocations, or a short value with CONN_HEALTH_CHECKS on containers.
  4. Cap instance concurrency and maximum instances so worst-case connections stay within the pooler's limits.
  5. Keep long-running work (migrations, admin scripts) on direct connections, not through the transaction pooler.

Step-by-step solution

1. Compute the worst case

max instances x connections per instance = peak client connections
Cloud Run: 100 instances x 1 worker x 4 threads = 400
Lambda:    500 concurrent executions x 1          = 500

Compare with the database's max_connections minus reserved superuser slots and connections for workers, cron, and monitoring. If the worst case exceeds it, you need a pooler, a concurrency cap, or both.

2. Use a transaction pooler

A transaction-mode pooler assigns a server connection only for the duration of each transaction and returns it to the pool afterwards. Hundreds of client connections can share 20 to 50 server connections.

DATABASES = {
    "default": {
        **dj_database_url.parse(os.environ["DATABASE_POOLER_URL"]),
        "CONN_MAX_AGE": 0,
        "DISABLE_SERVER_SIDE_CURSORS": True,
    }
}

DISABLE_SERVER_SIDE_CURSORS is required because QuerySet.iterator() uses named cursors that live across statements, which break when consecutive statements land on different server connections.

platform and poolerPoolerModeAWS Lambda, FargateRDS ProxymultiplexingNeonpooled endpointtransactionSupabaseSupavisor poolertransactionCloud Run + Cloud SQLPgBouncer or managedtransactionSelf-managedPgBouncertransactionUse the platform’s pooled connection string instead of the direct onedjango-deployment.com
Keep a direct connection string for migrations and long admin tasks.

3. Choose CONN_MAX_AGE per platform

  • Lambda and Vercel functions: execution environments freeze between invocations; a persistent connection may be dead or held by the pooler when the function thaws. Use CONN_MAX_AGE = 0 so each invocation opens and closes its connection (cheap through a pooler).
  • Cloud Run, Fargate, Kubernetes: instances stay alive and handle many requests; a short CONN_MAX_AGE (30 to 60 seconds) with CONN_HEALTH_CHECKS = True reuses connections efficiently.

4. Cap concurrency

On Cloud Run, set --max-instances and --concurrency so peak connections are bounded. On Lambda, use reserved concurrency for the function. On container autoscalers, set the maximum replica count from the connection budget. A queue of requests waiting for capacity is better than a database refusing connections.

5. Separate long-lived work

Migrations take advisory locks and may run long transactions; run them with a direct connection string in a one-off job, not through the transaction pooler. Same for pg_dump, data backfills, and admin shells with interactive transactions.

before scaling outPooled connection stringtransaction modeServer-side cursors disablediterator() safeCONN_MAX_AGE fits platform0 for functionsMax instances cappedfrom connection budgetDirect URL for migrationsseparate jobBounded concurrency plus a pooler keeps the database out of troubledjango-deployment.com
Watch the pooler’s own client and server connection metrics.

Explanation

PostgreSQL forks a backend process per connection, which makes connections relatively expensive and limits how many a server can hold. Serverless platforms scale the number of application instances rapidly and independently of the database, so connection counts follow traffic spikes. Transaction-mode pooling decouples the two: many clients, few server connections, with the constraint that session-level state cannot persist between transactions, which is why Django's server-side cursors must be disabled.

Edge cases / notes

  • Prepared statements. psycopg 3 can use server-side prepared statements; with older PgBouncer versions in transaction mode, disable them (prepare_threshold=None). PgBouncer 1.21+ supports protocol-level prepared statements.
  • Advisory locks. Libraries that use session-level advisory locks (some job queues) need session pooling or direct connections.
  • Cold starts. Opening a connection through a pooler is fast, but TLS handshakes still add latency; keep the pooler close to the app.

Next steps

FAQ

Why does serverless Django exhaust Postgres connections?

Each new instance opens its own connections, and autoscaling can create many instances quickly.

Why must I disable server-side cursors with a pooler?

Named cursors span several statements on one connection; transaction pooling may route those statements to different server connections.

Should CONN_MAX_AGE be 0 on Lambda?

Usually yes, because frozen execution environments make persistent connections unreliable.

Can migrations run through the pooler?

Prefer a direct connection for migrations and long administrative tasks.

Keep going