You deploy a Lambda function, attach it to a VPC so it can reach your RDS instance, and the first invocation dies with Task timed out after 3.00 seconds. No stack trace. No connection refused. Just silence for three seconds and then failure. It happens most often right after someone moves a working Lambda into a private subnet, or when a new function is created with a role that was copied from a non-VPC function and never got the ENI permissions added.
The 3.00 second number is the giveaway. That's the default Lambda timeout, not a network timeout. Lambda waited the full three seconds for the function to return, and the function was blocked inside a socket connect() call the whole time.
What's actually happening here
When you attach a Lambda to a VPC, Lambda needs to create an Elastic Network Interface (ENI) in each subnet you specified, then route all your function's outbound traffic through those ENIs. That ENI creation happens with permissions from the function's execution role, specifically ec2:CreateNetworkInterface, ec2:DescribeNetworkInterfaces, and ec2:DeleteNetworkInterface.
If those permissions are missing, Lambda can't build the ENI, the function has no network path at all, and your RDS connection just hangs until the function timeout fires. Same symptom if the ENI exists but the subnet's route table doesn't have a path to RDS, or if the RDS security group doesn't allow traffic from the Lambda's security group.
The reason people see this so often with RDS specifically is that RDS is usually in a private subnet, so the Lambda has to be in the VPC too. A Lambda that isn't in a VPC runs in AWS-managed networking and reaches public RDS endpoints fine. The moment you attach it to a VPC, you've opted into manual networking: subnets, route tables, security groups, NAT gateways for internet access, the works.
That three-second hang is the function's TCP connect() call waiting for SYN-ACK packets that never arrive. It's not a DNS problem unless you're also seeing UnknownHostException in CloudWatch.
The fix
Add ENI permissions to the execution role. Open IAM, find the role Lambda is using, and attach this inline policy. The AWS-managed
AWSLambdaVPCAccessExecutionRolecovers it, but if you're building least-privilege policies, this is the minimum:{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ec2:CreateNetworkInterface", "ec2:DescribeNetworkInterfaces", "ec2:DeleteNetworkInterface", "ec2:AssignPrivateIpAddresses", "ec2:UnassignPrivateIpAddresses" ], "Resource": "*" } ] }Confirm the subnets have a route to RDS. If RDS is in the same VPC and same subnet range, the local route handles it. If RDS is in a peered VPC, a Transit Gateway, or on-prem via VPN, you need an explicit route in the Lambda subnet's route table pointing at the peer/TGW/VPN. Check with:
aws ec2 describe-route-tables \ --filters "Name=association.subnet-id,Values=subnet-0abc123"Look at the DestinationCidrBlock matching your RDS CIDR. If it's not there, add it. If it points at a NAT gateway, that's a problem, NAT doesn't route to private RDS.
Fix the security groups. The RDS security group needs an inbound rule allowing the database port (5432 for Postgres, 3306 for MySQL, 1433 for SQL Server) from the Lambda's security group ID, not a CIDR. Using a security group reference is what makes this work when Lambda IPs change on every cold start. In the RDS console, edit the security group inbound rules and add a rule of type PostgreSQL (or whatever your engine uses) with the source set to the Lambda function's security group.
Give the function enough time. Once the ENI exists, connection establishment to RDS is fast, but a cold start with ENI attachment can take 1-2 seconds on its own. Bump the timeout to at least 10 seconds while you're debugging:
aws lambda update-function-configuration \ --function-name my-func \ --timeout 15You can dial it back later. If your handler is doing real work plus a DB query, 3 seconds was probably never enough anyway.
Use RDS Proxy or connection pooling. Lambda spins up a new execution environment per concurrency slot, and each one opens its own DB connection. Push past 100 concurrent invocations against a t3.medium RDS and you'll saturate
max_connections. If you're seeing timeouts only under load, it's this, not networking. RDS Proxy gives you a connection pool in front of the database that Lambda connects to instead.
If it still times out
Check CloudWatch Logs first. If you see UnknownHostException, it's DNS. Lambda in a VPC uses the VPC's DNS resolver, which needs enableDnsSupport and enableDnsHostnames both set to true on the VPC. Also check that the RDS endpoint you're using is the writer endpoint and not an endpoint from a different VPC.
If you see Connection refused, the route is fine but the security group is blocking. If you see nothing at all in logs before the timeout, your handler is stuck before it even prints, which usually means ENI creation is failing silently. Look at the CloudWatch metrics for the function: if InitDuration is missing entirely, the function never initialized, and that points back at IAM.
One more thing people miss: if your Lambda is in a private subnet and also needs to call any public AWS API (S3, Secrets Manager, DynamoDB), you need either a NAT gateway in a public subnet or a VPC endpoint. No NAT, no internet, no API calls, and you'll get timeouts on those too. The RDS fix gets you DB access, but it doesn't magically give the function a route to the outside world.