First, the 30-second fix: shorten your Lambda timeout and add a connection timeout
Yeah, I know it sounds backwards—the problem is a timeout, so why shorten it? Because you're probably waiting 30 seconds for a connection that will never come. The pool is exhausted, so every new connection just sits there until Lambda gives up. That's wasted time and money.
Do this:
- Go to your Lambda function's configuration and set the timeout to 10 seconds (or less, if your queries are fast).
- In your database connection code, set a connection timeout of 2–3 seconds. For example, with Python and psycopg2:
import psycopg2
conn = psycopg2.connect(
dbname='mydb',
user='myuser',
password='mypass',
host='myhost',
port=5432,
connect_timeout=2
)This way, your Lambda fails fast instead of hanging. You'll get a clear error message like timeout expired or connection timed out instead of a generic Lambda timeout. That tells you it's a connection issue, not a query issue.
But here's the thing: this doesn't solve the root problem. It just makes the failure faster. If you're okay with that for now, stop here. But you won't be, because your users are still hitting errors. So move on.
The 5-minute fix: use RDS Proxy
This is the real fix for 90% of cases. RDS Proxy sits between your Lambda and RDS, pooling connections on the database side. It's designed exactly for this—Lambda functions that open and close connections rapidly, exhausting the pool.
Here's the deal:
- Create an RDS Proxy in the AWS Console. It takes about 10–15 minutes to provision, but the setup itself is quick.
- Point your Lambda at the proxy endpoint instead of the RDS endpoint. You only need to change the host in your connection string.
- Make sure your Lambda can access the proxy via the same VPC and security groups.
Why does this work? RDS Proxy reuses connections across Lambda invocations. Instead of every Lambda creating a new connection, they share from a pool the proxy manages. This drops the number of connections to RDS dramatically—from hundreds down to a handful.
I've seen this fix a production outage in under 20 minutes. The only gotcha is that RDS Proxy adds a tiny bit of latency (like 1–2 ms), but that's nothing compared to a 30-second timeout.
One thing to watch: make sure your IAM role for Lambda has permission to connect to the proxy. You'll need rds-db:connect on the proxy resource. If you get a database connection refused error, that's your first suspect.
So, if you can, just do this. It's the cleanest solution and you don't have to touch your code (much).
The 15+ minute fix: rework your code to reuse connections
If you can't use RDS Proxy—maybe you're on a budget or you need that last bit of performance—then you have to fix the connection pooling in your Lambda code.
The root cause is usually a connection leak. You're probably creating a connection per invocation and not closing it properly. Or you're using a pool that doesn't work well with Lambda's lifecycle.
Here's what you do:
- Move the connection creation outside the handler. In Lambda, code outside the handler runs once per warm start. That's where your connection should live.
- Use a pooling library that supports connection reuse. For example, in Node.js, use
pgwithpg.Pool. In Python, use SQLAlchemy with a pool. Just make sure the max connections is low (like 1–2) because Lambda runs multiple instances. - Handle errors and close connections properly. Use
try/finallyorwithblocks to release connections back to the pool. If you don't, you'll leak connections on every invocation.
Here's a minimal Python example with SQLAlchemy:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(
'postgresql://user:pass@host:5432/db',
pool_size=1,
max_overflow=0,
connect_args={'connect_timeout': 2}
)
Session = sessionmaker(bind=engine)
def handler(event, context):
session = Session()
try:
result = session.execute('SELECT 1')
return {'status': 'ok'}
finally:
session.close()Notice that I'm creating the engine once, globally. Each invocation gets a session from the pool, uses it, and returns it. If your function already does this, then the problem might be that you're not setting max_overflow=0—that lets the pool grow beyond your limit, which defeats the purpose.
Also, check your RDS instance's max_connections setting. The default for PostgreSQL is max_connections=100. If you have many Lambda instances running concurrently, each with a pool of 10, you'll blow through that fast. Keep the pool size small, like 1–2 per Lambda instance, and rely on Lambda's concurrency to scale.
One more thing: don't forget to test with a load generator. You'll want to simulate 100+ concurrent invocations to see if the pool holds up. If it doesn't, you're back to RDS Proxy.
Quick rule of thumb: if you have more than 10 Lambdas accessing the same RDS, use RDS Proxy. It's not worth the headache of tuning pools.
Why this happens in the first place
Lambda scales by spawning new instances. Each instance can hold a connection pool. When you get a burst of traffic, Lambda spins up dozens of instances, each opening a handful of connections. That exhausts RDS's max connections fast—especially if you have other apps also using the same database.
I've seen this with a client who had a Node.js Lambda doing a simple query. They had the pool size set to 10. One traffic spike of 50 concurrent requests and the database was at 500 connections. RDS was on a small instance with max_connections=100. Chaos.
So, the root cause is always the same: too many connections, too little reuse. Fix that, and you're golden.