The Error
Your app tries to open a database connection and gets this:
FATAL: too many connections for role "myapp"
Or this variant:
FATAL: remaining connection slots are reserved for non-replication superuser connections
Same root cause. RDS has hit its connection ceiling and is refusing everyone else.
Root Cause
PostgreSQL on RDS sets a hard ceiling on concurrent connections based on instance memory. The formula: LEAST({DBInstanceClassMemory/9531392}, 5000). On a db.t3.micro, that's roughly 60โ80 connections total. Not much.
Hit that ceiling and every new connection attempt fails โ even if most existing connections are just sitting idle.
Common triggers:
- No connection pooler โ each thread or process holds its own connection
- Lambda cold starts โ every new execution opens a fresh connection instead of reusing one
- Connection leaks โ connections opened but never closed (often hidden in error paths)
- Multiple environments sharing one RDS instance (dev + staging + prod)
- ORM pool misconfigured across too many replicas โ 10 replicas ร 20 connections each = 200 connections, easy
Step 1: Check Current Connection Usage
Before fixing anything, see what's actually happening. Connect as a superuser or rds_superuser and run:
SELECT count(*), usename, state
FROM pg_stat_activity
GROUP BY usename, state
ORDER BY count DESC;
Compare against your limit:
SELECT current_setting('max_connections') AS max,
count(*) AS current
FROM pg_stat_activity;
If you're near capacity with dozens of idle connections, kill the stragglers to buy breathing room:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND usename = 'myapp'
AND query_start < now() - interval '10 minutes';
Fix 1: Add pgBouncer (Recommended)
pgBouncer sits between your app and RDS. Your app opens hundreds of "connections" to pgBouncer. pgBouncer funnels them into a small pool of real RDS connections โ say, 1000 app connections mapped to just 20 RDS connections.
Deploy it on an EC2 instance or as a sidecar container. Minimal config (/etc/pgbouncer/pgbouncer.ini):
[databases]
mydb = host=mydb.xxxx.us-east-1.rds.amazonaws.com port=5432 dbname=mydb
[pgbouncer]
listen_port = 5432
listen_addr = 0.0.0.0
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
reserve_pool_size = 5
server_idle_timeout = 600
The userlist.txt:
"myapp" "md5hashofpassword"
Point your app at pgBouncer's address instead of RDS directly. Done โ 1000 app connections, 20 real ones.
Running Lambda? Use RDS Proxy instead. It's AWS's managed pooler, built for serverless where you can't keep a pgBouncer process alive:
aws rds create-db-proxy \
--db-proxy-name myapp-proxy \
--engine-family POSTGRESQL \
--auth '[{"AuthScheme":"SECRETS","SecretArn":"arn:aws:secretsmanager:...","IAMAuth":"DISABLED"}]' \
--role-arn arn:aws:iam::123456789:role/rds-proxy-role \
--vpc-subnet-ids subnet-xxx subnet-yyy \
--vpc-security-group-ids sg-xxx
Fix 2: Increase max_connections in Parameter Group
Quick relief, not a real solution. Raise the limit via a custom parameter group:
aws rds modify-db-parameter-group \
--db-parameter-group-name myapp-params \
--parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot"
Apply the group and reboot:
aws rds modify-db-instance \
--db-instance-identifier mydb \
--db-parameter-group-name myapp-params \
--apply-immediately
aws rds reboot-db-instance --db-instance-identifier mydb
Don't go past 2ร the default without adding a pooler. PostgreSQL allocates shared memory per connection โ push it too high and you'll trade a connection error for an OOM crash.
Fix 3: Tune Your ORM Connection Pool
SQLAlchemy's defaults are often too generous for RDS. Tighten them:
from sqlalchemy import create_engine
engine = create_engine(
DATABASE_URL,
pool_size=5, # persistent connections per process
max_overflow=10, # temporary burst connections
pool_timeout=30, # seconds to wait before raising an error
pool_pre_ping=True, # drop stale connections before use
pool_recycle=1800, # recycle connections after 30 min
)
The math matters: (pool_size + max_overflow) ร replicas = your total RDS connection count. With 10 replicas and these settings, that's 150 connections. Size your instance accordingly.
Node.js with pg:
const { Pool } = require('pg');
const pool = new Pool({
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
Fix 4: Handle Lambda Connection Leaks
Lambda's execution model is the trap. Each cold start creates a new connection. Open it inside the handler and you risk a fresh connection on every invocation โ none of them closed cleanly.
Move the connection outside the handler so warm invocations reuse it:
import psycopg2
import os
# Outside the handler โ persists across warm invocations
conn = None
def get_connection():
global conn
if conn is None or conn.closed:
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
)
return conn
def handler(event, context):
db = get_connection()
# use db...
This helps, but it's still a band-aid. At high concurrency, hundreds of Lambda instances each hold their own connection. RDS Proxy (Fix 1) handles this more cleanly โ no handler changes needed.
Verification
After applying a fix, watch the connection count stabilize:
-- Run every few seconds to watch live
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
With pgBouncer running, check its stats panel:
psql -h localhost -p 5432 -U pgbouncer pgbouncer -c 'SHOW POOLS;'
Compare cl_active (client connections) against sv_active (server connections). Seeing 200 client connections multiplexed into 18 server connections means pgBouncer is working exactly as intended.
Prevention
- Always pool connections โ pgBouncer for long-running services, RDS Proxy for Lambda/serverless
- Enable
pool_pre_ping=True(SQLAlchemy) or equivalent to drop stale connections before they cause errors - Set
idle_in_transaction_session_timeoutto auto-kill stuck transactions:SET idle_in_transaction_session_timeout = '5min'; - Add a CloudWatch alarm on
DatabaseConnectionsโ alert at 80% ofmax_connections, not 100% - Don't run production workloads with multiple services on a
db.t3.microโ its 60โ80 connection cap will bite you

