Python Serverless on AWS Lambda · Running Python Functions in Production
Database Connections from Python
Why a function fails for minutes after a database failover, and how to manage relational connections in Python functions.
Scenario
Example app: a Python API uses PostgreSQL on Amazon RDS through psycopg. To avoid reconnecting on every request, the connection is opened at module level.
What you see in production
After a routine Multi-AZ failover (or a database restart), a large share of requests fail for several minutes with psycopg.OperationalError: server closed the connection unexpectedly, until the affected execution environments are recycled. During traffic spikes the database also hits its connection limit.
The naive code
import os, psycopg
conn = psycopg.connect(os.environ["DATABASE_URL"]) # opened once, never checked
def handler(event, context):
with conn.cursor() as cur:
cur.execute("select name, price from products where id = %s", (event["id"],))
return dict(zip(("name", "price"), cur.fetchone()))
Reproduce it
class Connection:
def __init__(self, generation): self.generation = generation
def query(self, db_generation):
if self.generation != db_generation:
raise ConnectionError("server closed the connection unexpectedly")
return "row"
def run(reconnect_on_error):
db_generation, conn, results = 1, Connection(1), []
for request in range(6):
if request == 2:
db_generation = 2 # failover: old connections are dead
try:
results.append(conn.query(db_generation))
except ConnectionError as e:
if not reconnect_on_error:
results.append("ERROR"); continue
conn = Connection(db_generation) # reconnect once and retry
results.append(conn.query(db_generation) + " (after reconnect)")
return results
print("no reconnect:", run(False))
print("reconnect: ", run(True))
Output
no reconnect: ['row', 'row', 'ERROR', 'ERROR', 'ERROR', 'ERROR'] reconnect: ['row', 'row', 'row (after reconnect)', 'row', 'row', 'row']
Root cause
A module-level connection lives as long as the execution environment, often for hours. Failovers, restarts, idle timeouts and network changes kill it silently; nothing re-establishes it, so every request in that environment fails. And because each environment holds a connection, total connections scale with Lambda concurrency.
The fix
- Keep a module-level connection for reuse, but validate and reconnect: catch connection errors, reconnect once and retry idempotent operations (or use a small pool with health checks, such as
psycopg_poolwithmax_size=1). - Put RDS Proxy in front of the database to pool connections across environments, speed up failover, and use IAM authentication; cap the function's concurrency to protect the database.
- Keep transactions short; never hold a transaction open across invocations.
import os, psycopg
_conn = None
def connection():
global _conn
if _conn is None or _conn.closed:
_conn = psycopg.connect(os.environ["PROXY_URL"], autocommit=True, connect_timeout=3)
return _conn
def query_product(product_id):
for attempt in (1, 2):
try:
with connection().cursor() as cur:
cur.execute("select name, price from products where id = %s", (product_id,))
return cur.fetchone()
except psycopg.OperationalError:
global _conn
_conn = None # drop the dead connection and retry once
if attempt == 2:
raise
Trade-offs
- Retrying is only safe for idempotent operations; for writes, use idempotency keys or let the error surface.
- RDS Proxy adds cost and some latency, and session features can pin connections.
- DynamoDB or HTTP data APIs avoid connection management entirely, at the cost of a different data model.
Detect it in production
- Error bursts after failovers or maintenance windows, concentrated in long-lived environments.
- Database connection counts relative to Lambda concurrency.
- RDS Proxy metrics (client and database connections, pinning).
Key points
- Module-level connections are reused but can die silently; detect and reconnect.
- Use RDS Proxy and cap concurrency for relational databases.
- Retry only idempotent operations; keep transactions short.
The connection-storm side of this problem is covered in the Node.js course's RDS Proxy lesson.
Further reading: Using Lambda with Amazon RDS · Amazon RDS Proxy
Practice questions
Why do requests keep failing after a database failover when the connection is opened at module level?
Answer
The module-level connection lives for the whole execution environment; after a failover it is dead, and nothing reconnects, so every request in that environment fails until it is recycled. Detect connection errors and reconnect.
When is it safe to retry a database operation after reconnecting?
Answer
When the operation is idempotent (reads, or writes guarded by idempotency keys or conditional logic). Retrying a non-idempotent write could apply it twice if the first attempt actually succeeded.
How does RDS Proxy help Python functions specifically during failovers?
Answer
Clients stay connected to the proxy, which reconnects to the new primary and can preserve client connections, shortening failover disruption, while also pooling connections across many execution environments.
Open in the app to mark lessons complete, take notes and track your progress.