Cloud Databases

AWS RDS Proxy: Connection Pooling for Lambda, Microservices, and High-Concurrency MySQL and PostgreSQL

A production guide to AWS RDS Proxy — how it works, when to use it, IAM authentication setup, and the connection pool sizing rules that prevent Lambda-induced database overload.

JusDB Team
January 16, 2023
11 min read
319 views

At 2:47 AM on a Tuesday, a fintech startup's on-call engineer got paged: their payment processing API was returning 500 errors. The culprit was not a code bug or a network partition — it was their AWS Lambda functions exhausting the maximum connection limit on their RDS PostgreSQL instance. Each Lambda invocation was opening a new database connection, holding it for the 250ms lifetime of the function, and then dropping it. With 400 concurrent Lambda invocations during a fraud detection spike, they blew past their max_connections limit of 300 and RDS began rejecting new connection attempts entirely. Their application had no connection pooling layer between Lambda and RDS.

This is the single most common database architecture mistake we see in serverless and containerized AWS environments. Lambda, ECS Fargate, and auto-scaling EC2 fleets all share the same problem: each compute unit wants its own database connection, and the total count can spike by an order of magnitude in seconds. RDS is not built for thousands of short-lived connections — it allocates memory and spawns OS processes per connection, and those resources are finite.

Amazon RDS Proxy is AWS's managed answer to this problem. It sits between your application and your RDS or Aurora database, multiplexes thousands of application connections onto a small pool of persistent database connections, and hands those connections back to the database only when a transaction is actually executing. In the fintech team's case, deploying RDS Proxy cut their peak active database connections from 400 to 17 during the same traffic spike. They never hit max_connections again.

This guide covers how RDS Proxy works internally, when connection pinning breaks multiplexing efficiency, how to set it up with AWS CLI and Terraform, and an honest comparison against self-managed alternatives like pgBouncer and ProxySQL.


TL;DR
  • RDS Proxy is a fully managed AWS connection pooler that sits between your application and RDS/Aurora, multiplexing thousands of app connections onto a much smaller pool of real database connections.
  • It supports MySQL, PostgreSQL, MariaDB, SQL Server, and their Aurora equivalents — but runs in transaction pooling mode only, which means session-level state (temporary tables, advisory locks, SET variables) does not survive across queries.
  • Connection pinning is the critical gotcha: certain operations (prepared statements, SET commands, LOCK TABLE) pin an application connection to a specific database connection, defeating multiplexing and degrading pool efficiency.
  • RDS Proxy integrates natively with IAM authentication and AWS Secrets Manager, eliminating hardcoded database credentials in application code.
  • RDS Proxy costs 18 cents per vCPU-hour of the underlying RDS instance — roughly a 30–40% surcharge on your RDS bill, which is worth it for serverless and bursty workloads but harder to justify for steady-state long-connection applications.
  • For EC2-hosted databases or workloads where cost is paramount, pgBouncer (PostgreSQL) and ProxySQL (MySQL) deliver comparable pooling without the managed overhead cost.

Background

Database connections are expensive. On PostgreSQL, each connection spawns a backend process consuming roughly 5–10 MB of shared memory and a file descriptor. On MySQL and MariaDB, each connection is a thread with its own stack allocation. The default max_connections on a db.t3.medium RDS PostgreSQL instance is around 170; on a db.r6g.large it is around 870. These ceilings exist because RAM is finite, and the PostgreSQL connection overhead is not amortizable — you pay the full cost per connection regardless of whether the connection is idle or executing a query.

Traditional application servers — Rails, Django, Spring Boot — maintain a fixed-size connection pool (typically 5–20 connections per application server instance) and reuse connections across requests. This works well when you control the number of application server processes. The model breaks down in two scenarios that have become dominant in modern AWS architectures:

  • Serverless (AWS Lambda): Each Lambda container maintains its own connection. With 500 concurrent Lambda invocations, you have up to 500 simultaneous connections to RDS, even if each is only active for 50ms per invocation. Lambda does not share connection pools across invocations; the connection pool lives in the container and dies with it.
  • Rapid horizontal scaling: ECS Fargate tasks or Kubernetes pods that scale from 10 to 200 instances in 60 seconds will attempt 200× their baseline connection count. Even with a pool of 10 connections per container, that is 2,000 simultaneous connections appearing in under a minute.

Before RDS Proxy, the workarounds were painful: run pgBouncer or ProxySQL on a dedicated EC2 instance (which is itself an availability risk and an operational burden), reduce per-Lambda connection pools to 1 (which causes contention), or simply over-provision RDS instance size to raise max_connections (expensive and treats the symptom, not the cause).

How RDS Proxy Works

RDS Proxy is a stateful, multi-AZ proxy fleet managed by AWS inside your VPC. It runs in the same VPC as your RDS instance but in AWS-managed subnets, exposing a single DNS endpoint to your application.

Connection Multiplexing

The core mechanism is connection multiplexing. Your application opens connections to the proxy endpoint — these are client connections. RDS Proxy maintains a separate, much smaller pool of database connections to the actual RDS instance. When a client connection needs to execute a transaction, the proxy borrows a database connection from its pool, routes the transaction, and then returns the database connection to the pool once the transaction commits or rolls back. The client connection stays open; only the database connection is borrowed and released.

In practice, a deployment with 500 Lambda client connections might maintain only 20 persistent database connections. Those 20 connections serve the 500 clients through time-multiplexing, because at any given millisecond, only a fraction of Lambda functions are in an active transaction. The ratio of client connections to database connections is the multiplexing factor — in serverless workloads, this commonly runs 20:1 to 50:1.

IAM Authentication

Instead of storing database usernames and passwords in application environment variables or Parameter Store, you can authenticate to RDS Proxy using IAM tokens. Your application generates a short-lived (15-minute) authentication token signed with its IAM role, and the proxy verifies it against IAM before establishing the client connection. The proxy itself maintains the actual database credentials, fetched from Secrets Manager.

Tip

IAM authentication to RDS Proxy is particularly valuable for Lambda functions. The Lambda execution role already has an IAM identity — you can grant it rds-db:connect permission on the proxy resource ARN and eliminate all database credential management from Lambda configuration entirely.

Secrets Manager Integration

RDS Proxy requires database credentials to be stored in AWS Secrets Manager. It reads the secret at proxy creation time and periodically re-fetches it to support automatic credential rotation. When Secrets Manager rotates a database password, RDS Proxy seamlessly updates its internal credentials without dropping existing connections — a significant operational improvement over application-managed credentials, where a rotation event requires a coordinated redeploy.

Connection Pinning

Connection pinning is the most important concept to understand before deploying RDS Proxy — and the one most teams discover only after they notice the multiplexing ratio is far worse than expected.

Multiplexing only works when the proxy can safely return a database connection to the pool after a transaction and hand it to a different client. For this to be safe, the proxy must be certain that no session-level state from the previous client remains on the database connection. When an operation sets session-level state that the proxy cannot fully track and reset, the proxy pins that client connection to that database connection for the lifetime of the client connection. Pinned connections cannot be pooled.

Warning — Operations that trigger pinning
  • SET statements: SET search_path = ..., SET LOCAL, SET SESSION, or any session variable modification. The proxy cannot reset these between transactions.
  • Prepared statements: Using the extended query protocol (binary prepared statements) causes pinning in PostgreSQL. Text-mode queries (simple query protocol) do not cause pinning. Many PostgreSQL drivers default to prepared statements — check your driver configuration.
  • LOCK TABLE outside a transaction block.
  • XA transactions and SAVEPOINT operations in some configurations.
  • Stored procedures that return multiple result sets (SQL Server and MySQL).
  • Any use of pg_advisory_lock() or similar session-scoped locks.
  • Temporary tables: CREATE TEMPORARY TABLE pins the connection for the session lifetime.

A high pin ratio means your effective multiplexing factor collapses toward 1:1, and RDS Proxy provides no benefit over a direct connection. Monitor the DatabaseConnectionsCurrentlySessionPinned CloudWatch metric. If this is consistently above 10–15% of total connections, audit your application for the pinning patterns above.

Setting Up RDS Proxy

Prerequisites

Before creating the proxy, you need:

  1. An RDS or Aurora instance in a VPC with subnets in at least two Availability Zones.
  2. A Secrets Manager secret containing the database credentials (JSON with username and password keys).
  3. An IAM role that grants RDS Proxy permission to read the secret.
  4. A VPC security group allowing inbound traffic on port 5432 (PostgreSQL) or 3306 (MySQL) from your application's security group.

Create a Secrets Manager Secret

bash
# Store RDS credentials in Secrets Manager
aws secretsmanager create-secret \
  --name "rds/myapp/db-credentials" \
  --description "RDS PostgreSQL credentials for myapp" \
  --secret-string '{
    "username": "myapp_user",
    "password": "supersecretpassword",
    "engine": "postgres",
    "host": "mydb.cluster-xyz.us-east-1.rds.amazonaws.com",
    "port": 5432,
    "dbname": "myapp_production"
  }'

Create the IAM Role for RDS Proxy

bash
# Create trust policy for RDS service
cat > rds-proxy-trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "rds.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

# Create the role
aws iam create-role \
  --role-name RDSProxySecretsRole \
  --assume-role-policy-document file://rds-proxy-trust-policy.json

# Attach inline policy to read the secret
aws iam put-role-policy \
  --role-name RDSProxySecretsRole \
  --policy-name ReadRDSSecret \
  --policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rds/myapp/db-credentials-*"
    }, {
      "Effect": "Allow",
      "Action": ["kms:Decrypt"],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/YOUR_KMS_KEY_ID",
      "Condition": {
        "StringEquals": { "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com" }
      }
    }]
  }'

Create the RDS Proxy via AWS CLI

bash
aws rds create-db-proxy \
  --db-proxy-name myapp-proxy \
  --engine-family POSTGRESQL \
  --auth '[{
    "AuthScheme": "SECRETS",
    "SecretArn": "arn:aws:secretsmanager:us-east-1:123456789012:secret:rds/myapp/db-credentials-AbCdEf",
    "IAMAuth": "ALLOWED"
  }]' \
  --role-arn "arn:aws:iam::123456789012:role/RDSProxySecretsRole" \
  --vpc-subnet-ids subnet-0abc123 subnet-0def456 subnet-0ghi789 \
  --vpc-security-group-ids sg-0proxy123 \
  --require-tls \
  --idle-client-timeout 1800 \
  --debug-logging false

# Register the RDS instance as the proxy target
aws rds register-db-proxy-targets \
  --db-proxy-name myapp-proxy \
  --db-instance-identifiers myapp-db-instance

Terraform Configuration

bash
resource "aws_db_proxy" "myapp" {
  name                   = "myapp-proxy"
  debug_logging          = false
  engine_family          = "POSTGRESQL"
  idle_client_timeout    = 1800
  require_tls            = true
  role_arn               = aws_iam_role.rds_proxy.arn
  vpc_security_group_ids = [aws_security_group.rds_proxy.id]
  vpc_subnet_ids         = aws_subnet.private[*].id

  auth {
    auth_scheme = "SECRETS"
    iam_auth    = "ALLOWED"
    secret_arn  = aws_secretsmanager_secret.rds_credentials.arn
  }
}

resource "aws_db_proxy_default_target_group" "myapp" {
  db_proxy_name = aws_db_proxy.myapp.name

  connection_pool_config {
    connection_borrow_timeout    = 120
    max_connections_percent      = 90
    max_idle_connections_percent = 50
    session_pinning_filters      = ["EXCLUDE_VARIABLE_SETS"]
  }
}

resource "aws_db_proxy_target" "myapp" {
  db_instance_identifier = aws_db_instance.myapp.identifier
  db_proxy_name          = aws_db_proxy.myapp.name
  target_group_name      = aws_db_proxy_default_target_group.myapp.name
}
Tip — EXCLUDE_VARIABLE_SETS

The session_pinning_filters = ["EXCLUDE_VARIABLE_SETS"] setting tells RDS Proxy not to pin connections when clients send SET commands that only affect the current session's query behavior (like SET TIME ZONE or SET search_path). This is safe for most applications and significantly reduces unintended pinning. Enable it unless your application relies on SET state persisting across separate transactions.

Configuration Best Practices

max_connections_percent

This parameter controls the maximum percentage of the RDS instance's max_connections that RDS Proxy will use for its database connection pool. The default is 100%, but setting it to 90% is recommended to reserve headroom for direct administrative connections (e.g., a DBA connecting directly to the instance for an emergency query without going through the proxy).

max_idle_connections_percent

Controls what fraction of the proxy's database connection pool can remain idle. Set this to 50% in most production environments. If you set it too low (e.g., 10%), the proxy aggressively closes idle database connections, which means it must frequently re-establish connections under bursty load, adding latency. Too high and you hold database connections open unnecessarily.

connection_borrow_timeout

The maximum time in seconds a client will wait for the proxy to provide a database connection from the pool. The default is 120 seconds, which is almost certainly too high for a web application — your application's own request timeout is probably 30 seconds. Set this to 10–30 seconds and treat a borrow timeout as an alerting signal that the proxy pool is saturated.

Important — idle_client_timeout

The proxy-level idle_client_timeout (set at proxy creation time, default 1800 seconds) closes client connections that have been idle for longer than the configured duration. This is intentional behavior — the proxy recycles idle Lambda connections that have no chance of being reused. However, some ORMs and connection pool libraries will attempt to reuse a connection the proxy has already closed, causing a confusing "connection reset" error. Ensure your application handles connection errors with retry logic, or configure its connection pool's keepalive interval to be shorter than the proxy's idle timeout.

Enabling IAM Authentication on the Application Side

bash
# Generate an IAM auth token for RDS Proxy (valid for 15 minutes)
# Used in application code to authenticate without a static password

aws rds generate-db-auth-token \
  --hostname myapp-proxy.proxy-xyz.us-east-1.rds.amazonaws.com \
  --port 5432 \
  --region us-east-1 \
  --username myapp_user

In your Lambda function, generate the token at cold-start time and cache it for up to 14 minutes. Re-generate before 15 minutes elapses. The token is a signed URL string; pass it as the database password in your connection string.

Verifying Proxy Connectivity

sql
-- Connect via the proxy endpoint and verify you are routed to the correct instance
SELECT current_database(), inet_server_addr(), version();

-- Check active connections visible through the proxy
SELECT count(*), state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state, wait_event_type, wait_event
ORDER BY count DESC;

RDS Proxy vs pgBouncer vs ProxySQL

RDS Proxy is not the only connection pooling option in an AWS environment. Here is an honest side-by-side comparison:

Criterion RDS Proxy pgBouncer ProxySQL
Supported databases MySQL, PostgreSQL, MariaDB, SQL Server, Aurora MySQL, Aurora PostgreSQL PostgreSQL only MySQL, MariaDB, Percona, Aurora MySQL
Pooling modes Transaction pooling only Session, transaction, and statement pooling Connection multiplexing; query-level routing rules
Managed overhead Fully managed by AWS. Multi-AZ by default. No EC2 to operate. Self-managed. Runs on EC2 or ECS. You own HA, patching, and monitoring. Self-managed. Typically deployed on dedicated EC2 instances with keepalived for HA.
Cost ~$0.018 per vCPU-hour of the underlying RDS instance. A db.r6g.2xlarge (8 vCPUs) running 24/7 adds ~$104/month for the proxy. EC2 instance cost only. A t3.small (~$15/month) handles >10,000 client connections for most workloads. EC2 instance cost. A c5.large (~$62/month) handles large-scale MySQL workloads with query routing.
Read/write splitting Read-only endpoint via Aurora reader endpoint; no query-level routing. No. Requires separate pgBouncer instances for writer and reader endpoints. Yes. Rule-based routing of SELECT queries to replicas, writes to primary. Native feature.
IAM / Secrets Manager Native AWS integration. IAM auth and Secrets Manager rotation built in. Manual. Requires scripting around credential rotation; no IAM support. Manual. Supports credential files and runtime variable updates; no native IAM.
Connection pinning / session mode Transaction mode only. Pinning reduces efficiency — avoid SET, prepared statements. All three modes available. Session mode has no pinning concern; transaction mode has the same prepared statement limitations. Multiplexing is disrupted by session-state changes similarly to pgBouncer transaction mode.
Best use case Lambda/Fargate/serverless, bursty workloads, teams wanting zero operational overhead, IAM-auth environments. Self-managed PostgreSQL on EC2, cost-sensitive teams, workloads needing session pooling mode. MySQL-heavy environments needing read/write splitting, query rewriting, or advanced routing rules.
Tip — Hybrid approach

For Aurora PostgreSQL with read replicas, consider combining RDS Proxy for the writer endpoint (handling Lambda traffic) with a separate pgBouncer instance for batch analytics workloads that direct read-heavy queries to the reader endpoint. RDS Proxy does not natively route individual queries to reader instances the way ProxySQL does for MySQL.

Key Takeaways

Key Takeaways
  • RDS Proxy solves the Lambda and serverless connection storm problem by multiplexing thousands of short-lived client connections onto a small, persistent pool of real database connections.
  • It only operates in transaction pooling mode. Session-level state — temporary tables, SET variables, advisory locks, prepared statements — pins a client connection to a single database connection and kills multiplexing efficiency. Monitor DatabaseConnectionsCurrentlySessionPinned in CloudWatch.
  • Use EXCLUDE_VARIABLE_SETS in the target group's session_pinning_filters to avoid pinning on benign SET commands from PostgreSQL drivers. Set max_connections_percent to 90 to reserve headroom for direct administrative connections.
  • IAM authentication via RDS Proxy eliminates hardcoded database passwords from Lambda and ECS task definitions. Grant your execution role rds-db:connect and generate tokens at runtime.
  • RDS Proxy adds roughly 30–40% to your RDS instance cost. For steady-state, long-lived connection workloads (traditional Rails apps on fixed EC2), this surcharge is hard to justify — pgBouncer on a t3.small accomplishes the same result for $15/month.
  • For MySQL workloads requiring read/write query-level routing, ProxySQL is still superior — RDS Proxy does not route individual SELECT queries to Aurora reader instances.

Working with JusDB on AWS RDS

Deploying RDS Proxy correctly requires more than running the AWS CLI commands above. The real work is in auditing your application code for pinning triggers, right-sizing the proxy's connection pool percentages for your traffic pattern, configuring CloudWatch alarms on borrow timeouts and pin ratios, and ensuring your ORM or database driver is not silently using the binary prepared statement protocol that causes pinning on PostgreSQL.

JusDB's team has helped engineering teams across fintech, e-commerce, and SaaS migrate from direct RDS connections to RDS Proxy, pgBouncer, and ProxySQL architectures — including the difficult edge cases around ORMs, Lambda cold starts, and Aurora failover behavior under a proxy. We work with both MySQL and PostgreSQL workloads on AWS.

MySQL & RDS Consulting Services Talk to an AWS Database Expert

If you are operating RDS or Aurora in production and are not sure whether connection pooling is limiting your throughput, we offer a database configuration review that includes connection pool sizing, max_connections headroom analysis, and a specific recommendation on whether RDS Proxy, pgBouncer, or ProxySQL is the right fit for your architecture.


Share this article