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

Concurrency versus database connectionsTry it yourself
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 DatabaseConnections approaching the maximum; errors such as too many connections.
  • RDS Proxy metrics: client connections, database connections, and pinned connections.
  • Correlate Lambda ConcurrentExecutions with 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.