Back to Blog

AWS RDS Proxy: Connection Pooling, IAM Auth, Failover, and Terraform Configuration

Deploy AWS RDS Proxy in production: connection pooling for serverless and containerized workloads, IAM database authentication, transparent failover during Aurora failover events, and Terraform IaC.

Viprasol Tech Team
December 20, 2026
13 min read

PostgreSQL has a hard limit on simultaneous connections — typically 100–1000 depending on instance size. Lambda functions and containerized workloads blow through this limit fast: 500 Lambda concurrent executions × 5 connections each = 2,500 connections, which will crash your database. RDS Proxy sits between your application and the database, multiplexing thousands of application connections into a small pool of real database connections. It also provides IAM-based authentication and cuts failover time from 30–60 seconds to under 10 seconds.

This post covers RDS Proxy in production: connection pooling configuration, IAM database authentication, Lambda integration, and Terraform IaC.

When to Use RDS Proxy

WorkloadWithout ProxyWith Proxy
Lambda (high concurrency)Connection exhaustionStable pool
ECS (many tasks)Connection exhaustionStable pool
Long-running serversOften fineMinimal benefit
Aurora Multi-AZ failover30–60s downtime< 10s failover
IAM DB auth (no passwords)Complex rotationNative support

Don't add RDS Proxy if: you have a small number of long-lived server processes (traditional API server). The proxy adds ~1–3ms latency and costs money.


1. Terraform Configuration

# infrastructure/rds-proxy/main.tf

# Secrets Manager secret for DB credentials (Proxy reads this)
data "aws_secretsmanager_secret" "db_credentials" {
  name = "/${var.environment}/app/database"
}

# IAM role for RDS Proxy to access Secrets Manager
resource "aws_iam_role" "rds_proxy" {
  name = "${var.project}-rds-proxy-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "rds.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "rds_proxy_secrets" {
  name = "${var.project}-rds-proxy-secrets"
  role = aws_iam_role.rds_proxy.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["secretsmanager:GetSecretValue"]
      Resource = data.aws_secretsmanager_secret.db_credentials.arn
    }]
  })
}

# RDS Proxy
resource "aws_db_proxy" "main" {
  name                   = "${var.project}-proxy"
  debug_logging          = var.environment != "prod"
  engine_family          = "POSTGRESQL"
  idle_client_timeout    = 1800  # 30 minutes — close idle app connections
  require_tls            = true  # Always encrypt in transit
  role_arn               = aws_iam_role.rds_proxy.arn
  vpc_security_group_ids = [aws_security_group.rds_proxy.id]
  vpc_subnet_ids         = var.private_subnet_ids

  auth {
    auth_scheme               = "SECRETS"
    description               = "DB master credentials"
    iam_auth                  = "REQUIRED"   # Enforce IAM auth (no password connections)
    secret_arn                = data.aws_secretsmanager_secret.db_credentials.arn
  }

  tags = {
    Environment = var.environment
    Project     = var.project
  }
}

# Proxy target group: connects proxy to the Aurora cluster
resource "aws_db_proxy_default_target_group" "main" {
  db_proxy_name = aws_db_proxy.main.name

  connection_pool_config {
    connection_borrow_timeout    = 120        # Max wait for a pool connection (seconds)
    max_connections_percent      = 100        # Use up to 100% of max_connections
    max_idle_connections_percent = 50         # Keep up to 50% idle connections warm
    session_pinning_filters      = [
      # Don't pin for SET commands (allows better multiplexing)
      # "EXCLUDE_VARIABLE_SETS"  ← uncomment if your app uses SET LOCAL
    ]
  }
}

resource "aws_db_proxy_target" "main" {
  db_proxy_name          = aws_db_proxy.main.name
  target_group_name      = aws_db_proxy_default_target_group.main.name
  db_cluster_identifier  = aws_rds_cluster.main.cluster_identifier
}

# Security group for proxy (allow inbound from Lambda/ECS SGs)
resource "aws_security_group" "rds_proxy" {
  name        = "${var.project}-rds-proxy"
  description = "RDS Proxy security group"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [
      aws_security_group.lambda.id,
      aws_security_group.ecs_tasks.id,
    ]
  }

  egress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }
}

output "proxy_endpoint" {
  value = aws_db_proxy.main.endpoint
}

☁️ Is Your Cloud Costing Too Much?

Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.

  • AWS, GCP, Azure certified engineers
  • Infrastructure as Code (Terraform, CDK)
  • Docker, Kubernetes, GitHub Actions CI/CD
  • Typical audit recovers $500–$3,000/month in savings

2. IAM Database Authentication

# Allow Lambda/ECS tasks to connect via IAM (no DB password needed)
resource "aws_iam_role_policy" "app_rds_connect" {
  name = "${var.project}-rds-connect"
  role = aws_iam_role.ecs_task.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["rds-db:connect"]
      Resource = [
        # Format: arn:aws:rds-db:region:account:dbuser:proxy-resource-id/db-user
        "arn:aws:rds-db:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dbuser:${aws_db_proxy.main.id}/${var.db_username}"
      ]
    }]
  })
}
// src/lib/db/iam-auth.ts
// Generate IAM auth token for RDS Proxy connection (no hardcoded password)
import { Signer } from '@aws-sdk/rds-signer';

const signer = new Signer({
  hostname: process.env.RDS_PROXY_ENDPOINT!,
  port: 5432,
  region: process.env.AWS_REGION!,
  username: process.env.DB_USERNAME!,
});

// Token expires in 15 minutes — cache and refresh before expiry
let cachedToken: string | null = null;
let tokenExpiresAt = 0;

export async function getIAMAuthToken(): Promise<string> {
  if (cachedToken && Date.now() < tokenExpiresAt - 60_000) {
    return cachedToken;
  }

  cachedToken = await signer.getAuthToken();
  tokenExpiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
  return cachedToken;
}

// Build database URL with IAM auth token as password
export async function getProxyConnectionUrl(): Promise<string> {
  const token = await getIAMAuthToken();
  const host = process.env.RDS_PROXY_ENDPOINT!;
  const user = process.env.DB_USERNAME!;
  const dbname = process.env.DB_NAME!;

  return `postgresql://${user}:${encodeURIComponent(token)}@${host}:5432/${dbname}?ssl=true&sslmode=require`;
}
// src/lib/db/client.ts — Prisma with RDS Proxy
import { PrismaClient } from '@prisma/client';
import { getProxyConnectionUrl } from './iam-auth';

let prisma: PrismaClient | null = null;

export async function getDb(): Promise<PrismaClient> {
  if (prisma) return prisma;

  const url = await getProxyConnectionUrl();

  prisma = new PrismaClient({
    datasourceUrl: url,
    // Limit connections per instance (proxy multiplexes across instances)
    // connection_limit set in the connection string or via Prisma env vars
  });

  return prisma;
}

// For Lambda: create once at cold start, reuse across invocations
let db: PrismaClient | null = null;

export async function getLambdaDb(): Promise<PrismaClient> {
  if (!db) {
    db = await getDb();
  }
  // IAM token needs refresh every 15 minutes
  // Prisma will reconnect automatically with new token if connection drops
  return db;
}

3. Session Pinning (and How to Avoid It)

Session pinning forces the proxy to use the same database connection for the entire client session — which kills the pooling benefit. The proxy pins sessions when it detects connection-modifying statements.

// ❌ Causes session pinning (SET affects connection state):
await db.$executeRaw`SET app.tenant_id = ${tenantId}`;

// ❌ Also pins:
await db.$executeRaw`SET LOCAL app.tenant_id = ${tenantId}`;
// SET LOCAL is transaction-scoped but still pins with RDS Proxy

// ✅ Alternative: pass tenant context via application parameters
// Option 1: Use ROW LEVEL SECURITY with a security-definer function
// Option 2: Include tenant_id in every WHERE clause explicitly
// Option 3: Use session_pinning_filters = ["EXCLUDE_VARIABLE_SETS"] in proxy config
//           (allows SET commands without pinning — less strict)

// If you MUST use SET LOCAL (e.g., for RLS):
// Add EXCLUDE_VARIABLE_SETS to session_pinning_filters in Terraform
// This tells proxy to not pin on SET commands — use carefully

⚙️ DevOps Done Right — Zero Downtime, Full Automation

Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.

  • Staging + production environments with feature flags
  • Automated security scanning in the pipeline
  • Uptime monitoring + alerting + runbook automation
  • On-call support handover docs included

4. Lambda Integration

// src/functions/api-handler/handler.ts
// Lambda with RDS Proxy — optimal connection reuse pattern

import { getLambdaDb } from '../../lib/db/client';

let dbInitialized = false;

// Warm up DB connection during init phase (before first invocation)
// Reduces cold-start latency for first request
export const handler = async (event: APIGatewayEvent): Promise<APIGatewayProxyResult> => {
  // Lazy init — connection reused across warm invocations
  const db = await getLambdaDb();

  try {
    const result = await db.product.findMany({
      where: { tenantId: event.requestContext.authorizer?.tenantId },
      take: 20,
    });

    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(result),
    };
  } catch (err) {
    console.error(err);
    return { statusCode: 500, body: JSON.stringify({ error: 'Internal error' }) };
  }
};

Cost Reference

ComponentMonthly cost (prod)Notes
RDS Proxy (db.t3.small)~$15Charged per vCPU of underlying DB
RDS Proxy (db.r7g.xlarge)~$1004 vCPU × $22/vCPU/mo
IAM tokens$0No extra charge
Secrets Manager reads< $1Proxy reads secret at startup
Without proxy (connection exhaustion)PricelessOutage cost

See Also


Working With Viprasol

Running Lambda or ECS workloads that are exhausting your Aurora connection limit? We deploy and configure RDS Proxy with IAM authentication, session pinning analysis to maximize multiplexing efficiency, and Terraform IaC — typically cutting connection count by 80%+ and enabling transparent failover for your multi-AZ Aurora clusters.

Talk to our team → | Explore our cloud solutions →

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need DevOps & Cloud Expertise?

Scale your infrastructure with confidence. AWS, GCP, Azure certified team.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

Making sense of your data at scale?

Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.