Node.js Serverless on AWS Lambda · Data, Concurrency and Cost
Relational Databases and RDS Proxy
How a traffic spike turns hundreds of Lambda instances into a database connection storm, and how to prevent it.
Scenario
Example app: a Lambda API reads and writes PostgreSQL on Amazon RDS. Each function instance opens one connection at init and reuses it. The database allows about 400 connections.
What you see in production
During a marketing campaign the API starts returning errors: too many connections from PostgreSQL, while the database's CPU is at only 30%. After the spike, hundreds of idle connections remain open for a while.
The naive code
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect(); // one connection per execution environment
export const handler = async (event) => {
const { rows } = await client.query("select * from products where id = $1", [event.pathParameters.id]);
return json(200, rows[0]);
};
Reproduce it
const dbMaxConnections = 400;
const concurrencyDuringSpike = [50, 200, 450, 900, 600, 120]; // concurrent function instances per minute
const proxyPoolSize = 120; // connections the proxy keeps to the database
const queryShareOfInvocation = 0.25; // share of each invocation spent in a query
for (const concurrency of concurrencyDuringSpike) {
const direct = concurrency; // one connection held per instance
const viaProxy = Math.min(proxyPoolSize, Math.ceil(concurrency * queryShareOfInvocation));
console.log(`instances ${String(concurrency).padStart(3)} | direct ${String(direct).padStart(3)}`,
direct > dbMaxConnections ? "EXHAUSTED" : "ok ", `| via proxy ${String(viaProxy).padStart(3)} ok`);
}
Output
instances 50 | direct 50 ok | via proxy 13 ok instances 200 | direct 200 ok | via proxy 50 ok instances 450 | direct 450 EXHAUSTED | via proxy 113 ok instances 900 | direct 900 EXHAUSTED | via proxy 120 ok instances 600 | direct 600 EXHAUSTED | via proxy 120 ok instances 120 | direct 120 ok | via proxy 30 ok
Root cause
Lambda scales by adding execution environments, each with its own connection, so connection count tracks concurrency, not query load. Relational databases have hard connection limits and each connection costs memory. Environments that are idle but not yet recycled keep their connections open, so the database also sees many idle connections.
The fix
Put Amazon RDS Proxy between Lambda and the database: functions connect to the proxy, which multiplexes many client connections onto a smaller pool of database connections, handles failover faster, and supports IAM authentication and Secrets Manager integration. Also:
- Cap the function's concurrency (reserved concurrency) to what the database can serve.
- Keep transactions short; long transactions pin proxy connections and reduce multiplexing.
- Consider alternatives that avoid connection management: DynamoDB, Aurora Serverless with the Data API (HTTP), or Aurora DSQL.
// Connect to the proxy endpoint, authenticating with an IAM token instead of a stored password.
import { Signer } from "@aws-sdk/rds-signer";
const signer = new Signer({ hostname: process.env.PROXY_HOST, port: 5432, username: "app" });
const pool = new pg.Pool({ host: process.env.PROXY_HOST, user: "app", database: "shop", max: 1, ssl: true,
password: () => signer.getAuthToken() });
Trade-offs
- RDS Proxy adds cost and a small amount of latency, and some session features (prepared statements across transactions, session variables) cause connection pinning, reducing its benefit.
- Capping concurrency protects the database but turns excess load into throttling, which callers must handle.
- Data APIs remove connection management but have their own limits and latency characteristics.
Detect it in production
- Database
DatabaseConnectionsapproaching the maximum; errors such astoo many connections. - RDS Proxy metrics: client connections, database connections, and pinned connections.
- Correlate Lambda
ConcurrentExecutionswith connection counts.
Key points
- Lambda connections scale with concurrency; databases have hard limits.
- Use RDS Proxy (or an HTTP data API) for relational databases.
- Cap concurrency and keep transactions short; watch for pinning.
Further reading: Amazon RDS Proxy · Using Lambda with Amazon RDS
Practice questions
Why do Lambda functions exhaust relational database connections during traffic spikes?
Answer
Each execution environment holds its own connection, and Lambda adds environments as concurrency rises, so connection count grows with concurrency rather than with actual query load, quickly exceeding the database's connection limit.
How does RDS Proxy help, and what reduces its effectiveness?
Answer
It pools and multiplexes many client connections onto fewer database connections and speeds up failover. Session-level features such as some prepared statements, temporary tables or session variables pin a client to a database connection, reducing multiplexing.
Besides RDS Proxy, what can protect a relational database from Lambda scaling?
Answer
Capping the function's concurrency with reserved concurrency, keeping transactions short, queuing writes through SQS with limited consumer concurrency, or using HTTP-based data APIs or databases designed for this access pattern.
Open in the app to mark lessons complete, take notes and track your progress.