How to Connect AWS Lambda to RDS: Complete Step by Step Guide
By Braincuber Team
Published on March 31, 2026
In this comprehensive tutorial, you'll learn how to communicate with AWS RDS from AWS Lambda using AWS CDK as your Infrastructure as Code tool. AWS Lambda is a serverless, event-driven compute service that lets you run code without provisioning servers, while AWS RDS is a managed relational database service supporting MySQL, Postgres, Oracle, SQL Server, and more. This complete tutorial covers VPC setup, security groups, secrets management, and RDS Proxy for optimal performance.
What You'll Learn:
- Why use RDS with Lambda instead of DynamoDB
- How to create a VPC with private and public subnets using AWS CDK
- Setting up an RDS Postgres database instance in a private subnet
- Configuring Lambda function properties and security groups
- Managing database credentials securely with AWS Secrets Manager
- Writing Lambda function code to communicate with the database
- How to use RDS Proxy to improve performance by 10x
Why Use RDS with Lambda?
Most serverless architectures use DynamoDB as a data store to reduce costs and eliminate database server maintenance. However, using DynamoDB for all projects involving Lambda isn't always possible. Here's why you might choose RDBMS instead:
Dynamic Access Patterns
With DynamoDB, you must design querying patterns in advance. This isn't always possible as your product evolves based on customer feedback. RDBMS allows dynamic access patterns without changing existing models.
Advanced Query Functionality
DynamoDB doesn't provide flexibility in writing queries — you can't do GROUP BY functionality as you do in RDBMS. RDBMS has many built-in functionalities so you don't need external systems.
Existing Database & Familiarity
If you have an existing RDBMS database, you wouldn't want to migrate to DynamoDB without a compelling reason. Most developers are familiar with SQL, and you have a wide range of databases to choose from.
Entity Relationships
RDBMS allows relationships between entities with foreign keys to restrict invalid data from getting stored in the database.
Flexible Queries
A new entity can be brought in without much change to existing models. GROUP BY and other SQL functions work out of the box.
Project Architecture Overview
In almost all cases, your RDBMS database will be in a private subnet of the Virtual Private Cloud (VPC) so that no one from outside can access it. Your Lambda function containing business logic will also be in a private subnet. We'll use Postgres as our database, but the process is applicable for any database (MySQL, Oracle, MS SQL, etc.).
Prerequisites
Only basic knowledge of AWS CDK and TypeScript is required for this tutorial. You need to have an AWS account to create AWS resources. We'll use TypeScript with AWS CDK as our Infrastructure as Code tool.
Step 1: Create a Virtual Private Cloud (VPC)
We'll create 2 subnets — a private subnet and a public subnet. In the private subnet, we'll place our Postgres database. When you create a subnet of type PRIVATE_WITH_EGRESS in AWS CDK, it will also create a NAT Gateway in the public subnet.
const vpc = new ec2.Vpc(this, 'VpcLambda', {
maxAzs: 2,
subnetConfiguration: [
{
cidrMask: 24,
name: 'privatelambda',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 24,
name: 'public',
subnetType: ec2.SubnetType.PUBLIC,
},
],
});
Why Use NAT Gateway for Internet Connectivity?
The NAT Gateway allows only outbound connections from your private subnet to the internet. No one can initiate connections to your private subnet from the public internet. You need internet access to reach Secrets Manager's public endpoint for retrieving database credentials. NAT Gateway can also be reused for Lambda to call external APIs.
Step 2: Create an RDS Database Instance
We'll create a small instance type for this tutorial. In production, you'd likely use larger instances. We create a new security group for the database to control who can access it and through which port.
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DbSecurityGroup', {
vpc,
});
const databaseName = 'cloudtechsimplified';
const dbInstance = new rds.DatabaseInstance(this, 'Instance', {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_13,
}),
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE3,
ec2.InstanceSize.SMALL
),
vpc,
vpcSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
}),
databaseName,
securityGroups: [dbSecurityGroup],
credentials: rds.Credentials.fromGeneratedSecret('postgres'),
maxAllocatedStorage: 200,
});
The fromGeneratedSecret method creates a secret in Secrets Manager with the username passed as a parameter. We want the database username to be postgres, so we pass that value. Finally, we allocate 200GB of storage space for the database.
Step 3: Configure Lambda Function Properties
We'll use Node.js 16 for our Lambda function with a 3-minute timeout (instead of the default 3 seconds) and 256 MB of memory. Since aws-sdk is provided by the Lambda runtime, we exclude it during bundling. We install the pg npm package for communicating with Postgres.
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
'aws-sdk', // Use the 'aws-sdk' available in Lambda runtime
'pg-native', // We don't need pg-native
],
},
runtime: Runtime.NODEJS_16_X,
timeout: Duration.minutes(3), // Default is 3 seconds
memorySize: 256,
};
const lambdaSG = new ec2.SecurityGroup(this, 'LambdaSG', {
vpc,
});
const rdsLambdaFn = new NodejsFunction(this, 'rdsLambdaFn', {
entry: path.join(__dirname, '../src/lambdas', 'rds-lambda.ts'),
...nodeJsFunctionProps,
functionName: 'rdsLambdaFn',
environment: {
DB_ENDPOINT_ADDRESS: dbInstance.dbInstanceEndpointAddress,
DB_NAME: databaseName,
DB_SECRET_ARN: dbInstance.secret?.secretFullArn || '',
},
vpc,
vpcSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
}),
securityGroups: [lambdaSG],
});
Security Best Practice: Never Pass Passwords as Environment Variables
We're NOT passing the database password as an environment variable. Instead, we pass the Secret ARN and fetch the actual password dynamically at runtime from Secrets Manager for better security.
Step 4: Set Up Permissions and Security Groups
Lambda needs permissions to fetch the database password from Secrets Manager. We also need to configure security groups to allow Lambda to connect to the RDS instance through port 5432.
// Grant Lambda permission to read the database secret
dbInstance.secret?.grantRead(rdsLambdaFn);
// Allow Lambda to connect to RDS on port 5432
dbSecurityGroup.addIngressRule(
lambdaSG,
ec2.Port.tcp(5432),
'Lambda to Postgres database'
);
The grantRead line creates a role for Lambda with two permissions: DescribeSecret and GetSecretValue from Secrets Manager. This allows Lambda to retrieve the database password before connecting to the database.
Step 5: Write Lambda Function Code to Communicate with the Database
The actual Lambda function code is straightforward. We use the pg package to communicate with Postgres. Before initiating the database connection, we fetch the secret string from Secrets Manager, parse the JSON to extract the password, then connect and run a query.
import * as AWS from 'aws-sdk';
import { Client } from 'pg';
export const handler = async (event: any, context: any): Promise => {
try {
const host = process.env.DB_ENDPOINT_ADDRESS || '';
console.log(`host:${host}`);
const database = process.env.DB_NAME || '';
const dbSecretArn = process.env.DB_SECRET_ARN || '';
const secretManager = new AWS.SecretsManager({
region: 'us-east-1',
});
const secretParams: AWS.SecretsManager.GetSecretValueRequest = {
SecretId: dbSecretArn,
};
const dbSecret = await secretManager.getSecretValue(secretParams).promise();
const secretString = dbSecret.SecretString || '';
if (!secretString) {
throw new Error('secret string is empty');
}
const { password } = JSON.parse(secretString);
const client = new Client({
user: 'postgres',
host,
database,
password,
port: 5432,
});
await client.connect();
const res = await client.query('SELECT $1::text as message', [
'Hello world!',
]);
console.log(res.rows[0].message); // Hello world!
await client.end();
} catch (err) {
console.log('error while trying to connect to db');
}
};
Finally, we run a simple SELECT query on our database. To test, log in to your AWS console, select the Lambda service, choose your function (rdsLambdaFn), and click the Test button. You'll be able to see the logs confirming the connection.
Step 6: Use RDS Proxy to Improve Performance
Lambda is well-suited for functions that don't take much time, with a maximum timeout of 15 minutes. However, initiating a database connection every time Lambda is invoked creates significant load on the RDS server, especially with high invocation rates from SQS queues or other event sources. This reduces performance significantly.
Instead of directly creating connections from Lambda to the database, we can place an RDS Proxy between Lambda and the RDS database. The RDS Proxy maintains a pool of connections so that consumers can get pre-created connections instead of creating new ones each time.
Reduced Database Load
No need to create a new connection for every Lambda invocation. The load on the RDS server is reduced significantly.
10x Performance Improvement
Before RDS Proxy: ~500ms connection time. After RDS Proxy: ~50ms. That's a 10x improvement in Lambda performance.
Creating the RDS Proxy
We need to create the RDS Proxy and add the database instance as the proxy target. We pass the database secret to the proxy since it maintains connections, and use the same security group to open port 5432.
const dbProxy = new rds.DatabaseProxy(this, 'Proxy', {
proxyTarget: rds.ProxyTarget.fromInstance(dbInstance),
secrets: [dbInstance.secret!],
securityGroups: [dbSecurityGroup],
vpc,
requireTLS: false,
vpcSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
}),
});
// Update Lambda environment to use proxy endpoint
environment: {
DB_ENDPOINT_ADDRESS: dbProxy.endpoint, // Changed from dbInstance
DB_NAME: databaseName,
DB_SECRET_ARN: dbInstance.secret?.secretFullArn || '',
},
No Lambda Code Changes Required
You don't need to change your Lambda code at all. Simply update the endpoint environment variable from dbInstance.dbInstanceEndpointAddress to dbProxy.endpoint. The Lambda will now connect to the proxy instead of the database directly.
| Metric | Without RDS Proxy | With RDS Proxy |
|---|---|---|
| Connection Time | ~500ms | ~50ms |
| Database Load | High (new connection per invocation) | Low (connection pooling) |
| Lambda Code Changes | N/A | None required |
Step 7: Test the Lambda Function
Now you can log in to your AWS console for testing. Select the Lambda service and choose your function — in our case, rdsLambdaFn. You don't need to worry about the event property for this tutorial since we're not using it in our Lambda function code.
Navigate to Lambda Console
Go to the AWS Lambda service in your AWS console and select your function (rdsLambdaFn).
Click Test
Click the Test button. You'll see logs confirming the connection and the "Hello world!" message from the query result.
Verify Performance
If using RDS Proxy, you should see the endpoint pointing to the proxy instead of the database instance. Connection time should be around 50ms instead of 500ms.
Note About Initial Connection Time
It may take additional time when getting the initial connection from RDS Proxy. However, any subsequent connections will be fast since they're pulled from the connection pool. This is expected behavior and part of how connection pooling works.
Frequently Asked Questions
Why use RDS with Lambda instead of DynamoDB?
RDS is better when you have dynamic access patterns that evolve over time, need SQL features like GROUP BY, or have an existing RDBMS database. DynamoDB requires designing query patterns in advance and lacks flexible querying capabilities.
Why use RDS Proxy with Lambda?
RDS Proxy maintains a connection pool, reducing database load and improving Lambda performance by 10x (from ~500ms to ~50ms connection time). Without it, each Lambda invocation creates a new database connection, significantly increasing RDS server load.
How do I securely manage database credentials in Lambda?
Never pass database passwords as environment variables. Instead, store credentials in AWS Secrets Manager and pass only the Secret ARN to Lambda. Grant Lambda read permissions to fetch the password dynamically at runtime using the grantRead method.
Why does Lambda need a NAT Gateway for RDS in the same VPC?
Lambda needs internet access to reach AWS Secrets Manager's public endpoint for retrieving database credentials. The NAT Gateway allows outbound connections from the private subnet while preventing inbound connections from the public internet.
Does this tutorial work with databases other than Postgres?
Yes, the architecture and process remain the same for MySQL, Oracle, MS SQL, and other RDBMS supported by AWS RDS. You only need to change the database engine in the CDK code and use the appropriate npm package (like mysql2 for MySQL) in your Lambda function.
Need Help with AWS Serverless Architecture?
Our experts can help you design, implement, and optimize your AWS Lambda and RDS infrastructure for maximum performance, security, and cost efficiency.
