Fix AWS Lambda in VPC Has No Internet Access: The NAT Truth
If your Lambda function sits inside a VPC and can't reach the internet, the fix is almost never inside Lambda — it's a routing problem in the VPC itself: a private subnet with no path to a NAT gateway, or (the part that trips up almost everyone) a "public" subnet that isn't actually giving your function anything. Here's the part nobody tells you until you've burned an afternoon on it: putting a VPC-connected Lambda function in a public subnet does nothing for internet access, because Lambda never assigns your function a public IP address in the first place. The internet gateway route sitting right there in that subnet is completely useless to it.
Jake found this out the hard way on a Tuesday. He'd built a small Lambda function that looks up a customer's trade-in phone value from a supplier's pricing API, and he'd stuck it in the VPC he'd already set up for his repair-tracking database — because that's the VPC his RDS instance lives in, and it seemed logical to just reuse it. The function deployed fine. It ran fine in the console test. Then it sat there timing out, every single time, for three days, while a customer waited at the counter for a trade-in quote that never came back.
"I didn't even touch the internet part," Jake said. "I just told it which VPC to use. Why would that break anything?"
Why a VPC-connected Lambda function loses internet access in the first place
By default, every Lambda function runs inside a VPC that Lambda itself owns and manages — you never see it, you never configure it, and it has a normal route to the public internet. That's why a brand-new Lambda function, before you touch any networking settings, can call any public API on the internet with zero setup.
The moment you attach that function to a VPC in your account — because it needs to talk to an RDS database, an ElastiCache cluster, or some other resource that lives in a private subnet — Lambda stops using its own managed VPC for that traffic. Your function now only has access to whatever your VPC can reach. If your VPC has no path to the internet, your function has no path to the internet. Nothing about the function's code changed. Nothing about its IAM role changed. Only its network neighborhood changed, and that neighborhood decides everything from here on.
♂️ Jake's Reality Check
"So attaching my Lambda to a VPC actually took internet access away from it? That feels backwards. Shouldn't more networking mean more access, not less?"
Yes, it took it away — and that's by design, not a bug. A VPC is a private, isolated network. Attaching your function to one is you telling AWS "only let this function see what's inside this private box." Internet access was never part of that box unless you build a door to it.
The network interfaces Lambda creates to plug your function into your VPC are called Hyperplane Elastic Network Interfaces (ENIs for short — a network interface, or ENI, is just the virtual equivalent of a network cable and port your function plugs into). Each one gets a private IP address from the subnet you picked. It never gets a public IP address, and it can't use an internet gateway even if one is attached to the VPC, because an internet gateway only routes traffic for resources that have a public IP to route to. Your function's ENI simply doesn't have one — full stop, regardless of which subnet you chose.
The one-question diagnostic: which kind of subnet did you actually pick?
Before you touch any AWS console screen, answer this one question, because it decides which of the fixes below you actually need: is the subnet you attached your function to a public subnet (one with a route straight to an internet gateway) or a private subnet (one with no such route, or one that routes 0.0.0.0/0 to a NAT gateway instead)?
⚠️ What this actually breaks
If you picked a public subnet thinking it would give your function internet access, it won't — ever, no matter how you configure the route table. Connecting a function to a public subnet doesn't give it internet access or a public IP address, full stop, according to AWS's own Lambda documentation. This is the single most common wrong turn in this whole topic, and it wastes hours because everything about the setup looks correct.
Ethan put it to Jake this way: "Think of the internet gateway like the shop's front door onto the street. A public subnet is a room in the shop with a straight hallway to that door. Your Lambda function, though, doesn't get handed a coat and shoes to walk out that door — it doesn't have a public IP, so it's not allowed through, no matter how short the hallway is. It needs someone else to carry its package out for it. That's the NAT gateway's whole job."
So: if you're in a public subnet, moving to a private subnet plus a NAT gateway is the fix. If you're already in a private subnet with no NAT path, you just need to add the NAT gateway and the route. Either way, the destination is the same — a private subnet, with a 0.0.0.0/0 route pointed at a NAT gateway that itself sits in a public subnet with its own route to an internet gateway.
Method 1: NAT gateway — the default, AWS-managed fix
A NAT gateway (Network Address Translation gateway — a managed AWS service that swaps your private-subnet traffic's private IP for its own public IP before sending it out, then routes the reply back) is what most teams reach for first, and for a mixed workload it's usually the right call. It's fully managed by AWS: no patching, no scaling decisions, no server to keep alive.
If you're starting from scratch: the Create VPC workflow
If you don't already have a VPC set up for this, the fastest path is the Amazon VPC console's "VPC and more" option, which builds every piece in one pass — subnets, NAT gateway, internet gateway, and the route table entries — instead of you wiring each one by hand.
- Open the Amazon VPC console and choose Create VPC.
- For Resources to create, choose VPC and more.
- Give the VPC a name and either keep the suggested IPv4 CIDR block or enter your own.
- Under subnet configuration, choose 2 Availability Zones (AWS recommends at least two for high availability), 2 public subnets, and 2 private subnets.
- For NAT gateways, choose 1 per AZ if you want resilience — one NAT gateway failure then only takes out traffic in its own zone.
- For VPC endpoints, keep the default of an S3 Gateway endpoint. It's free, and it means S3 traffic never has to touch your NAT gateway at all (more on why that saves real money below).
- Choose Create VPC and let it finish provisioning.
Then attach your function to the private subnets it just created — never the public ones. In the Lambda console: open your function, go to the Configuration tab, choose VPC, select the VPC, pick the private subnets, and choose a security group that allows outbound traffic. AWS's own guidance is explicit here: for subnets, select all private subnets, because the private subnets can access the internet through the NAT gateway, while connecting a function to a public subnet doesn't give it internet access.
If you already have a VPC: verify, then patch the gaps
Most readers land here with an existing VPC, not a blank slate. In that case, don't rebuild — verify what's actually there first. Open the VPC console, choose your VPC ID, scroll to the Resource map section, and open each route table associated with a subnet. You're looking for two separate route tables, each satisfying one condition:
- One route table with a route sending 0.0.0.0/0 to an internet gateway (
igw-xxxxxxxxxx) — this marks the associated subnet as public. - A different route table sending 0.0.0.0/0 to a NAT gateway (
nat-xxxxxxxxxx) that itself sits in a public subnet — this marks the associated subnet as private-with-internet-access, which is exactly what your Lambda function needs.
If either piece is missing, you build it manually: create the internet gateway and attach it to the VPC, add the 0.0.0.0/0-to-internet-gateway route to the public subnet's route table, create the NAT gateway inside that public subnet with an allocated Elastic IP, and then add the 0.0.0.0/0-to-NAT-gateway route to the private subnet's route table. Note the detail that catches people out: the NAT gateway itself is associated with a public subnet, but the route table entry that uses it lives in the private subnet's route table, not the public one.
What changed between versions
- Before: a NAT gateway was always a single, zonal resource — one gateway lived in one Availability Zone, and teams needing resilience across zones had to run one NAT gateway per zone.
- Now: AWS also offers a Regional NAT gateway option that spans multiple Availability Zones under one resource, billed per active AZ-hour rather than per separate gateway, and its CloudWatch metrics carry an
AvailabilityZonedimension alongside the gateway ID so you can see per-zone behavior even though it's one resource. - What that means for the steps above: the zonal, one-per-AZ pattern in the Create VPC workflow above still works exactly as documented and remains the most common setup; the regional option is worth a look if you're standing up a new multi-AZ VPC and want one resource to manage instead of several.
How to know the fix actually worked
Once the routing is in place, the way to confirm it from inside your own function is to have it make one outbound HTTP request to a public endpoint and log the status code it gets back. If your function returns a 200, the request went out and came back — which means the private subnet, the NAT gateway, the route table entry, and the security group are all correctly lined up. If instead the function times out after a few seconds with no response at all, something upstream of your code is still blocking the path, and no amount of code-level troubleshooting will fix it — you need to go back to the route tables.
Method 2: NAT instance and unmanaged alternatives — cheaper, but you own the upkeep
A NAT instance is the older approach: a plain EC2 instance sitting in your public subnet, running network address translation itself instead of AWS doing it for you as a managed service. Your private subnet's route table points its 0.0.0.0/0 route at that instance's network interface instead of at a managed NAT gateway.
The appeal is cost for very light, spiky, or dev-only traffic, since you're paying for a small EC2 instance's hourly rate instead of the NAT gateway's combined hourly-plus-per-GB charge. The tradeoff is that you now own everything AWS was doing for you: patching the instance, monitoring whether it's still healthy, manually failing over if it dies, and sizing it correctly so it doesn't become the bottleneck. For a production Lambda function serving customer-facing traffic, that's usually not a good trade. For a personal project or a low-traffic staging environment where a NAT gateway's roughly $32-a-month baseline feels wasteful, it can make sense.
Beyond the plain EC2-instance approach, a handful of open-source, self-managed NAT instance images exist in the wider community as drop-in alternatives to a managed NAT gateway. Evaluate any of them the same way you'd evaluate any other unmanaged component you're adding to a production path: who maintains it, how actively, what happens the day it stops receiving updates, and whether the operational burden of running it yourself is genuinely worth whatever it saves versus the managed NAT gateway. For a shop like Jake's, where nobody on staff is watching infrastructure dashboards at 2 a.m., that tradeoff usually points straight back to the managed NAT gateway despite the higher sticker price.
Method 3: skip NAT for S3 and DynamoDB with a free Gateway endpoint
This is the method almost nobody reaches for, and it's often the best one. VPC endpoints let your VPC privately connect to a supported AWS service without needing an internet gateway, a NAT device, a VPN connection, or Direct Connect at all — the traffic never leaves the Amazon network in the first place.
For Amazon S3 and Amazon DynamoDB specifically, AWS provides Gateway-type VPC endpoints, and they carry no hourly charge and no per-gigabyte data processing charge. If your entire reason for wanting internet access was "my function needs to read from an S3 bucket" or "my function needs to hit a DynamoDB table," you may not need a NAT gateway at all — you need a Gateway endpoint routed into your private subnet's route table, and nothing else changes about your function's VPC configuration.
✅ Why this is the one to use for S3 and DynamoDB traffic
A NAT gateway charges a per-gigabyte data processing fee on every byte that crosses it, in either direction, regardless of where that traffic is ultimately headed. Sending S3 traffic through a NAT gateway when a free Gateway endpoint could carry it instead is money spent for nothing — AWS's own pricing page uses exactly this scenario as its worked example, and explicitly notes that routing that same traffic through a Gateway-type VPC endpoint avoids the NAT gateway's data processing charge entirely, since Gateway-type endpoints carry no data processing or hourly charge of their own.
Method 4: Interface endpoints (PrivateLink) for everything else
S3 and DynamoDB get the free Gateway endpoints. Most other AWS services — Secrets Manager, Systems Manager Parameter Store, STS, and dozens more — use Interface endpoints instead, built on AWS PrivateLink. These aren't free, but they're often cheaper than a NAT gateway if the only thing your function needs the internet for is talking to other AWS services.
An Interface endpoint bills two ways: an hourly charge per endpoint per Availability Zone it's provisioned in, and a per-gigabyte data processing charge on top. On AWS's own PrivateLink pricing page, the worked example bills each endpoint's network interface at $0.01 per hour, with data processing starting at $0.01 per GB for the first petabyte of combined monthly traffic across all Interface endpoints in the region. Compare that to a NAT gateway, which AWS's own VPC pricing page lists at $0.045 per NAT-gateway-hour plus $0.045 per GB of data processed, on top of any standard data transfer charges — the Interface endpoint's per-GB rate runs less than a quarter of the NAT gateway's for the exact same AWS-service traffic, though the calculus changes once you're also hitting the open internet, since Interface endpoints only cover named AWS or PrivateLink-published services, never arbitrary public URLs.
Set one up by opening the VPC console, choosing Endpoints, creating a new endpoint for the specific service you need (Secrets Manager, for example), selecting your VPC and the private subnets your function uses, and attaching a security group that allows inbound HTTPS (port 443) from your Lambda function's security group. Your function's own security group needs the matching outbound rule to port 443 toward that endpoint's security group.
♂️ Jake's Reality Check
"Wait — so I could skip the NAT gateway completely and just add endpoints for the two or three AWS services I actually talk to?"
Only if every single thing your function calls is an AWS service with an endpoint available, or S3/DynamoDB. The moment your function needs to reach one external, non-AWS API — like Jake's supplier pricing lookup — you still need a NAT gateway (or a NAT instance) for that piece, because Interface endpoints only front named AWS or PrivateLink services, not the open internet. Plenty of real functions end up using both: endpoints for AWS calls, a NAT gateway for the one external API.
Method 5: the fix most people skip — don't attach to a VPC at all
This is the fix that costs nothing, requires no NAT gateway, no endpoints, and no route table surgery — and it's the right answer for a surprising number of functions that ended up in a VPC by accident, or by habit, rather than by genuine need. Every Lambda function has internet access by default, before you configure any VPC settings at all. VPC access is something you deliberately opt into, specifically because your function needs to reach a private resource that lives inside that VPC — an RDS instance, an ElastiCache cluster, an internal load balancer — and nothing else.
If Jake's phone-shop function only calls an external pricing API and never touches his RDS database, it never needed a VPC config in the first place. Removing it is a one-line change: run an update-function-configuration command with an empty list of subnets and security groups, and the function goes back to using Lambda's own managed, internet-connected VPC. No NAT gateway bill, no route tables to maintain, no Hyperplane ENI cold-start behavior to think about.
⚠️ What this actually breaks
Don't remove the VPC config if the function genuinely does need to reach something private inside that VPC — you'll trade a solvable internet-access problem for a much harder "my function can't see my database anymore" problem. Check what the function's code actually calls before assuming it's safe to detach.
Method 6: outbound IPv6 without a NAT gateway
If your VPC uses dual-stack subnets — subnets with both an IPv4 and an IPv6 CIDR block — Lambda supports outbound IPv6 traffic directly, and IPv6 doesn't use NAT the way IPv4 does. Instead, a private dual-stack subnet routes its outbound IPv6 traffic (destination ::/0) to an egress-only internet gateway, which is essentially a one-way door: it lets traffic leave for the internet over IPv6 but never lets anything initiate a connection inward.
To use it, select Allow IPv6 traffic for dual-stack subnets when configuring your function's VPC settings (or pass Ipv6AllowedForDualStack=true in the CLI), and make sure every subnet you selected has both an IPv4 and an IPv6 CIDR block — Lambda requires that pairing on every selected subnet when this option is on. This won't help if the service you're calling is IPv4-only, which describes most of the public internet today, but for IPv6-reachable endpoints it removes the NAT gateway's per-GB data processing charge from that slice of traffic entirely, since an egress-only internet gateway carries no such fee.
Decision table: which method actually fits your case
| Your situation | Use this | Why |
|---|---|---|
| Only needs S3 or DynamoDB | Gateway VPC endpoint | Free, no NAT bill at all |
| Only calls other AWS services (Secrets Manager, SSM, STS, etc.) | Interface endpoints | Cheaper per-GB than NAT, no open-internet exposure |
| Calls external, non-AWS APIs | NAT gateway | Only method that reaches the open internet reliably at production scale |
| Low-traffic dev/staging, cost-sensitive | NAT instance | Cheaper baseline, but you manage patching and failover yourself |
| Doesn't touch any private VPC resource at all | Remove the VPC config | Free, and Lambda's default managed VPC already has internet access |
| Dual-stack VPC, IPv6-reachable destinations | Egress-only internet gateway | No per-GB NAT processing charge on that traffic |
What this actually costs, side by side
Jake's first question when Ethan mentioned "NAT gateway" was the honest one: "What's this going to cost me a month?" The answer depends entirely on which method you pick, and the gap between them is bigger than most people expect.
| Option | Hourly / base charge | Per-GB data charge |
|---|---|---|
| NAT gateway | $0.045 per NAT gateway-hour | $0.045 per GB processed |
| Interface (PrivateLink) endpoint | $0.01 per endpoint ENI, per hour | $0.01 per GB (first 1 PB/month, region-wide) |
| Gateway endpoint (S3, DynamoDB) | $0.00 | $0.00 |
| No VPC config at all | $0.00 | $0.00 |
All figures above come straight from AWS's own Amazon VPC pricing page and AWS PrivateLink pricing page. Run the numbers on a single always-on NAT gateway and you land at roughly $0.045 times 24 hours times 30 days — call it about $32-and-change a month before a single byte of traffic crosses it, plus $0.045 for every gigabyte that does. Standard data transfer charges for traffic leaving AWS to the public internet apply on top of all of this, separately, regardless of which method carries it there.
Put actual numbers on Jake's situation: his trade-in-pricing function calls one small supplier API a few hundred times a day, exchanging a few kilobytes per call. Even generously rounding that up to a few gigabytes a month, the NAT gateway's per-GB charge barely registers next to its own $32-and-change monthly baseline — the hourly charge, not the data, is what dominates for a light workload like his. That's the opposite of what most people assume walking in, and it's worth knowing before you spend time optimizing data volume on a function whose real cost driver is simply the gateway sitting there provisioned around the clock.
✅ Why this is the one to check first
If Jake's function calls one supplier API and nothing else, one NAT gateway shared across every function in that VPC covers it, and $32-and-change a month for a small shop is a rounding error next to what a lost customer costs. If the traffic is heavier or if several functions are all hammering S3 and DynamoDB through that same NAT gateway unnecessarily, the Gateway endpoint is the free fix that should have been there from day one.
When you've done everything above and it still times out
This is where most guides stop, and where the real frustration usually starts. A correctly configured NAT gateway with a correct route table entry still doesn't guarantee your function can reach the internet, because several more layers sit between your function and success.
Security group has no outbound rule
Every ENI Lambda creates uses whatever security group you attached when you configured the function's VPC settings. If that security group's outbound rules don't allow the traffic you're trying to send — commonly, HTTPS on port 443 to 0.0.0.0/0 — the request never leaves the ENI, NAT gateway or no NAT gateway. A default security group that was cloned from something restrictive is the single most common reason "everything looks right" still fails.
Network ACL blocking the return traffic
Network ACLs are stateless, unlike security groups, which means an ACL that allows outbound traffic on port 443 but doesn't explicitly allow the ephemeral-port range inbound will silently swallow the response. If you've customized your subnet's network ACL rather than using the VPC default, check both directions.
DNS resolution not enabled for endpoints
If you're routing traffic to an AWS service through a PrivateLink Interface endpoint, that endpoint only helps if DNS resolves the service's hostname to the endpoint's private IP instead of its public one. That depends on your VPC's DNS attributes being correctly configured for private hosted zones — worth checking directly if Secrets Manager, SSM Parameter Store, or a similar service specifically is the one still failing while general internet calls succeed (or vice versa).
A NAT gateway that exists but nothing routes to it
It's entirely possible to create the NAT gateway itself, forget the private subnet's route table entry, and see the resource sitting there "provisioned and available" in the console while doing absolutely nothing for your function's traffic. A NAT gateway is billed the moment it's provisioned, whether or not anything is actually routed to it — so an unused, forgotten one quietly costs money while solving nothing.
Dedicated-tenancy VPC
Lambda functions can't connect directly to a VPC that uses dedicated instance tenancy. If your organization's VPC was set up with dedicated tenancy for compliance reasons, the workaround is peering it to a second VPC with default tenancy and attaching your function to that second VPC instead.
The subnet quietly ran out of IP addresses
Every subnet's IP capacity is smaller than its CIDR block suggests, because AWS reserves the first four addresses and the last address in every subnet for network, router, DNS, and broadcast purposes. A /24 subnet with 256 total addresses gives you 251 usable ones, not 256 — and each Lambda Hyperplane ENI, EC2 instance, load balancer node, and RDS instance in that subnet consumes one. A high-concurrency Lambda function sharing a small subnet with other resources can run the subnet out of addresses, at which point new ENI creation fails outright and the function can't even reach the Pending-to-Active transition, let alone the internet. If you're seeing intermittent creation failures rather than clean timeouts, check subnet IP usage before anything else.
The cold-start question nobody asks until it bites
Even after your networking is correct, attaching a Lambda function to a VPC changes its lifecycle in a way that surprises people. The first time you attach a function to a particular combination of subnet and security group, Lambda has to create a Hyperplane ENI for that combination, and while that's happening the function sits in a Pending state — you can't invoke it yet. That creation can take a few minutes the first time.
The upside: any other function in your account using that exact same subnet-and-security-group combination reuses the same Hyperplane ENI rather than triggering a new creation, so the pain is mostly a one-time cost per unique combination, not per function. The detail worth knowing for cost and reliability planning: if a function sits idle for 14 days, Lambda reclaims the unused ENI and marks the function Inactive; the next invocation has to wait through ENI recreation before it can run. Design around this rather than assuming a VPC-attached function is always instantly warm.
♂️ Jake's Reality Check
"So even after I fix the internet access, my function might just be slow the first time someone uses it?"
Only the very first invocation against a fresh subnet-and-security-group combination, or after 14 days of complete silence. If your function gets invoked at least occasionally, this practically never shows up again after that first setup.
The permissions piece people forget entirely
Attaching a function to a VPC isn't purely a networking action — Lambda's service role needs permission to create and manage those ENIs on your behalf, or the whole configuration will fail before it ever gets to a routing problem. The AWSLambdaVPCAccessExecutionRole AWS managed policy covers this, and the Lambda console attaches it automatically when you enable VPC access on function creation. If you're doing this through the AWS CLI, AWS SAM, or attaching VPC access to an existing function, you need to add that policy — or the equivalent individual permissions (ec2:CreateNetworkInterface, ec2:DescribeNetworkInterfaces, ec2:DescribeSubnets, ec2:DeleteNetworkInterface, ec2:AssignPrivateIpAddresses, and ec2:UnassignPrivateIpAddresses) — to the function's execution role yourself, before the VPC attach step will succeed.
⚠️ What this actually breaks
Those same EC2 permissions, once granted to the execution role for ENI management, are implicitly available to your function's own code too — meaning your function code could technically call those EC2 APIs itself. If that's a concern for your security posture, AWS documents a deny policy using the lambda:SourceFunctionArn condition key that blocks the function's code from using these permissions while still letting the Lambda service manage the ENI lifecycle on your behalf.
Do you even need Lambda in a VPC? Ask this before anything else
Every one of the fixes above assumes your function genuinely needs VPC access. It's worth stepping back and confirming that before spending an hour on route tables. A function needs a VPC configuration if and only if it must reach a resource that only exists inside that private network — an RDS or Aurora database without public access, an ElastiCache Redis or Memcached cluster, an internal-only Elasticsearch/OpenSearch domain, or an EC2-hosted service with no public endpoint.
If your function only calls public APIs, only reads and writes S3 or DynamoDB, or only talks to other AWS services that have their own public or PrivateLink endpoints, attaching it to a VPC at all adds cost, adds the cold-start behavior described above, and adds an entire category of networking problems that a function outside a VPC simply never has. This single question — "does my code touch anything private?" — resolves more of these support tickets than any NAT gateway configuration ever will.
Does packaging your function as a container image change any of this?
No. A function's VPC configuration — the subnets, security groups, and the optional dual-stack IPv6 setting — is a property of the function itself, set through the same VpcConfig field on create-function or update-function-configuration regardless of whether your code is deployed as a .zip archive or as a container image. None of the routing rules above change based on how you packaged the code. If your container-image function can't reach the internet, the diagnostic and the fixes in this post apply exactly the same way — the networking layer sits entirely below the packaging layer and doesn't know or care which one you used.
The hybrid setup: private resources AND the internet, together
Jake's actual function needs both: his RDS database, which lives in the VPC, and an external pricing API on the open internet. This is the most common real-world case, and it's simpler than it sounds once you separate the two concerns. The function attaches to the same private subnets that give it a path to RDS (RDS security groups reference the Lambda function's security group, or vice versa, to allow that traffic). Those same private subnets also route 0.0.0.0/0 to the NAT gateway, which handles the outbound call to the external pricing API. One VPC attachment, one set of private subnets, and two completely separate kinds of traffic riding on the same network path — private traffic staying inside the VPC to reach RDS, and internet-bound traffic getting handed off to the NAT gateway at the route table.
A different problem: reaching a resource in someone else's AWS account
Every fix above assumes the internet-facing resource lives outside AWS, or in a VPC you own. Sometimes it doesn't — Jake's supplier, for instance, might eventually offer a private connection into their own AWS account instead of a public API, since that beats a public internet call on both security and performance. AWS's documented pattern for this is a Lambda function configured to use a VPC peering connection into that other account's VPC, without exposing either VPC to the internet. The tutorial AWS publishes for this scenario walks through connecting two accounts with a peering connection over IPv4, configuring a Lambda function that isn't already connected to a VPC, and setting up DNS resolution so the function can reach resources that don't have static IPs of their own.
Doing this needs permission on both sides of the relationship: to create and update a VPC and its supporting resources in your own account, to update your function's execution role and VPC configuration, to create the peering connection from your account, and matching permissions on the resource owner's side to accept that peering connection and update their own VPC configuration to allow it. AWS's own guidance is blunt about the stakes here too: allowing access between accounts or VPCs affects the security posture of both sides, so this is a plan worth reviewing against both organizations' security requirements before you build it, not after.
Doing all of this without clicking through the console every time
Once you've built the console version once and understand exactly what it created, the next reasonable step is defining it as code so a new environment doesn't mean repeating every click by hand. AWS's own CloudFormation reference for a NAT gateway shows the whole thing in three resources: the NAT gateway itself, the Elastic IP it needs, and the route that sends the private subnet's traffic to it.
NATGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NATGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet
Tags:
- Key: stack
Value: production
RouteNATGateway:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NATGateway
On the Lambda side, AWS SAM exposes the exact same VpcConfig property that the console and CLI use, so a function's private subnets and security groups can be defined in the same template as the NAT gateway that gives them internet access:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./lambda_function/
Handler: lambda_function.handler
Runtime: python3.12
VpcConfig:
SecurityGroupIds:
- !Ref MySecurityGroup
SubnetIds:
- !Ref MyPrivateSubnet1
- !Ref MyPrivateSubnet2
Policies:
- AWSLambdaVPCAccessExecutionRole
Keeping the NAT gateway, the route, and the function's VPC config in the same template means the private subnet's route table entry can never quietly drift out of sync with the function the way it can when each piece gets clicked into place separately across different console sessions. If you're using a different infrastructure-as-code tool, look for the equivalent NAT gateway, route, and Lambda function resources in that tool's own reference documentation — the underlying AWS resources and their relationships are identical no matter which tool declares them.
Monitoring the NAT gateway so the next failure doesn't sneak up on you
Getting the NAT gateway working once isn't the end of the story if your Lambda function's traffic grows. AWS publishes a specific set of CloudWatch metrics for every NAT gateway, and two of them are worth watching precisely because of how Lambda behaves at scale: many concurrent invocations of the same function, all sharing the same NAT gateway, can open a lot of simultaneous outbound connections at once.
The metric to watch for that exact scenario is ErrorPortAllocation — AWS's own documentation describes it as the number of times the NAT gateway could not allocate a source port, and states plainly that a value greater than zero indicates too many concurrent connections are open through the NAT gateway. If your Lambda function suddenly starts failing its outbound calls under load, after having worked fine at low concurrency, this metric — not the route tables, not the security group — is where to look first. The companion metric, PacketsDropCount, tracks packets the NAT gateway dropped outright; AWS's guidance is to compare it against total inbound packet volume and treat anything above roughly 0.01 percent of total traffic as worth investigating against the AWS Service Health Dashboard, since at that point the drops may reflect an issue with the NAT gateway service itself rather than anything in your configuration.
♂️ Jake's Reality Check
"My function worked fine in testing and then started failing once real customers were using it at the same time. Is that the same NAT gateway problem?"
It's exactly the shape of problem ErrorPortAllocation exists to catch. A NAT gateway that handled a handful of test invocations without issue can genuinely run into port allocation pressure once dozens of concurrent Lambda invocations are all opening outbound connections through it at the same moment. Set a CloudWatch alarm on that metric before it becomes a live-customer problem instead of after.
A related question: can I get my Lambda function a static outbound IP?
This comes up constantly alongside the internet-access question, because plenty of external APIs require an IP allowlist before they'll accept your requests at all. The Hyperplane ENIs Lambda creates for your function have private IP addresses only, and you can't treat them as static IPs — they're shared infrastructure managed and reused across multiple functions in your account, so they aren't stable or unique to your function.
The fix piggybacks directly on the NAT gateway setup above: a NAT gateway is assigned a single Elastic IP address, and that Elastic IP is exactly what shows up as the source address when your VPC-attached function's traffic reaches the public internet. Give your NAT gateway one Elastic IP, route your private subnet's Lambda functions through it, and every outbound call from those functions appears to come from that one stable, allowlist-friendly address.
Before you open the door: what to check first
Every method in this post widens what your VPC-attached function is allowed to reach. Before opening a NAT gateway or an internet-facing endpoint, scope the function's security group to exactly the ports and destinations it needs — not a blanket allow-all outbound rule inherited from a template. Remember, too, that the ENI-management permissions granted to your execution role for VPC attachment are implicitly usable by your function's own code, which is exactly why the deny-policy pattern mentioned earlier exists for teams that want to close that gap. For a function handling anything sensitive — customer records, payment references, supplier credentials — treat the internet-access decision as a security decision first and a connectivity decision second, and revisit it any time the function's code changes what it calls.
Frequently asked questions
Why doesn't my Lambda function have internet access by default once it's in a VPC?
Because attaching a function to a VPC in your account replaces Lambda's own internet-connected managed VPC with your VPC for that function's networking. Your VPC only has whatever paths you've built into it — internet access included.
Does putting my Lambda function in a public subnet give it internet access?
No. Lambda functions never receive a public IP address regardless of which subnet they're attached to, and an internet gateway route is useless to a resource that has no public IP to route. Only a private subnet with a route to a NAT gateway (or the equivalent for IPv6) gives a VPC-attached function a way out.
What's the cheapest way to give a VPC-attached Lambda function internet access?
If the function doesn't actually need to reach any private VPC resource, removing the VPC configuration entirely is free and gives it Lambda's own internet-connected default networking back. If it does need VPC access and only ever calls S3 or DynamoDB, a Gateway VPC endpoint is also free. A NAT gateway is the option that costs money, and it's only strictly necessary for reaching the open internet or non-Gateway-endpoint AWS services.
Do I need a NAT gateway just to have my VPC-attached Lambda function read from S3?
No. Set up a Gateway VPC endpoint for S3 instead, and route your private subnet's traffic to it. It carries no hourly or per-gigabyte charge, unlike sending that same traffic through a NAT gateway.
Can I use a cheaper NAT instance instead of a managed NAT gateway?
Yes, a NAT instance is a regular EC2 instance running network address translation, and it can be cheaper for light or dev-only traffic. The tradeoff is that you take over patching, monitoring, and manual failover — none of which a managed NAT gateway requires of you.
What does the timeout error actually look like when internet access is the problem?
The function simply times out with no response — a message resembling "Task timed out after 3.01 seconds," with no exception, no error code, no useful stack trace pointing at your code. That silence is itself the signal: your code executed correctly up to the point of the outbound call, and the network path just never delivered a response.
Does my VPC-attached Lambda function need internet access to reach other AWS services?
Not necessarily. Many AWS services are reachable through PrivateLink Interface endpoints, and S3 and DynamoDB through free Gateway endpoints, entirely without a route to the public internet. You only need internet access (via a NAT gateway or similar) for services that don't offer a VPC endpoint, or for anything outside AWS entirely.
How do I know if my Lambda function actually needs to be in a VPC?
Check what it connects to. If any part of its code talks to an RDS or Aurora database, an ElastiCache cluster, or another resource that only has a private IP inside your VPC, it needs the VPC config. If everything it touches is public internet APIs, S3, DynamoDB, or other AWS services with public endpoints, it doesn't — and attaching it to a VPC anyway only adds cost and complexity.
What happens if I create a NAT gateway but forget to add the route in my private subnet's route table?
The NAT gateway sits there fully provisioned and billing you for every hour it exists, while doing absolutely nothing for your function's traffic. Traffic from the private subnet still has nowhere to go, and the function still times out exactly as before. Always verify the private subnet's route table has the 0.0.0.0/0-to-NAT-gateway entry, not just that the gateway exists.
Can I share one NAT gateway across several Lambda functions?
Yes. A NAT gateway serves every resource in every private subnet that routes traffic to it, regardless of whether that resource is a Lambda function, an EC2 instance, or anything else. Most accounts run one NAT gateway per public subnet (or one per Availability Zone for resilience) shared by everything in the matching private subnets, rather than one gateway per function. Just watch the ErrorPortAllocation metric as more functions share the same gateway under concurrent load.
Are VPC endpoints actually cheaper than a NAT gateway?
For traffic to S3 or DynamoDB through Gateway endpoints, yes — completely free versus a NAT gateway's hourly and per-GB charges on the same traffic. For traffic to other AWS services through Interface endpoints, the per-GB rate is lower than a NAT gateway's, though you're also paying a small hourly fee per endpoint per Availability Zone. Endpoints only cover named AWS or PrivateLink services, though — they can't replace a NAT gateway for calls to the open internet.
Why does my function take longer to start the first time after I add a VPC configuration?
The first time a function uses a new combination of subnet and security group, Lambda has to create a Hyperplane ENI for it, and the function stays in a Pending state until that finishes — which can take a few minutes. Subsequent invocations, and other functions reusing the same subnet-and-security-group combination, don't pay that cost again unless the function sits idle for 14 days and the ENI gets reclaimed.
Can my VPC-attached Lambda function reach the internet over IPv6 without a NAT gateway?
Yes, if your VPC subnets are dual-stack (both an IPv4 and IPv6 CIDR block). Route outbound IPv6 traffic to an egress-only internet gateway instead of a NAT gateway, and enable the "Allow IPv6 traffic for dual-stack subnets" option on your function's VPC configuration. This only helps for destinations reachable over IPv6, which excludes a large share of the public internet today.
What IAM permissions does Lambda need to attach a function to a VPC?
The function's execution role needs permission to create and manage network interfaces — covered by the AWS managed policy AWSLambdaVPCAccessExecutionRole, or the equivalent individual EC2 permissions if you're building a custom policy. Without this, the VPC attachment itself fails before routing even becomes a factor.
My NAT gateway and route tables look correct, but my function still times out. What else could it be?
Check the function's security group for an outbound rule that actually permits the traffic you're sending (commonly port 443 to 0.0.0.0/0). Then check the subnet's network ACL in both directions, since ACLs are stateless and can silently drop return traffic even when outbound is allowed. Also confirm the subnet hasn't run out of usable IP addresses, and if you're calling an AWS service through a PrivateLink endpoint, confirm your VPC's DNS settings are resolving to the endpoint rather than the public address.
Is it a security risk to give a VPC-attached Lambda function internet access?
It widens the function's exposure to whatever it's allowed to call outbound, which is exactly why "does this function actually need internet access" is worth asking before configuring it. Scope the security group's outbound rules as tightly as the function's actual calls require rather than leaving a broad allow-all rule, and remember that the EC2 permissions granted for ENI management are implicitly available to your function's code too, which AWS documents a deny-policy pattern for if that's a concern.
Revision note. Written September 2026,. AWS occasionally adjusts per-GB and per-hour rates, and new endpoint types get added over time, so it's worth a quick check against the live AWS pricing page before you budget against the numbers above. If you've been staring at a timed-out function wondering what you broke — you didn't break anything, the network just needed one more piece, and you'll have it working within the hour.