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.
- Put a transaction-mode pooler between Django and PostgreSQL: RDS Proxy, PgBouncer, Neon or Supabase pooled endpoints, Cloud SQL with a pooler.
- With transaction pooling, set
DISABLE_SERVER_SIDE_CURSORS = Trueand avoid session state (SETwithoutLOCAL, advisory locks across transactions). - Use
CONN_MAX_AGE = 0on functions that freeze between invocations, or a short value withCONN_HEALTH_CHECKSon containers. - Cap instance concurrency and maximum instances so worst-case connections stay within the pooler's limits.
- 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.
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 = 0so 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) withCONN_HEALTH_CHECKS = Truereuses 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.
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
- Diagnose exhaustion with Fix PostgreSQL Too Many Connections in Django.
- Run your own pooler with Set Up PgBouncer for Django.
- Use a serverless database with Connect Django to Neon Postgres.
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.