What it means
Attaching a function to a VPC means handing Lambda a set of subnets and a set of security groups. Lambda validates them before it creates or updates anything, and rejects the whole call if any identifier does not check out. Nothing is created, nothing runs, and nothing reaches CloudWatch — this lives entirely in the deployment output.
The word "not valid" covers more ground than it appears to. It does not only mean "does not exist". The most frequent cause is a set of identifiers that are each individually real and cannot legally be combined: a security group from one VPC alongside subnets from another. A security group belongs to exactly one VPC, and Lambda cannot place a network interface in a subnet using a group from somewhere else. Both identifiers describe existing resources; the pairing is what is rejected.
That happens more often than it sounds because of how these values are assembled. Subnets usually come from an application or networking stack, while the security group is frequently created later, by hand or in a different module, and defaults to the account's default VPC unless told otherwise. Everything is individually correct, the deploy fails, and the error points at the subnet — which is the half that is right.
Region is the other quiet one. Subnet and security group IDs carry no region information in their format, so subnet-0a1b2c3d from a staging account in another region looks exactly as plausible as the correct value, and refers to nothing where you are deploying.
It is worth separating this from its close neighbour before reaching for a different ID. SubnetIPAddressLimitReachedException means the subnet is entirely correct and has no free addresses left. The messages read similarly, they arrive from the same call, and they want opposite responses: one needs a corrected identifier, the other needs a bigger CIDR. Checking AvailableIpAddressCount distinguishes them in a single command.
Where you'll see it
Deployment output
Not in CloudWatchThis failure happens before the function runs, so nothing about it reaches CloudWatch Logs — there is no invocation, and no log group entry to find. Once the deployment succeeds and the function starts running, the rest of this index covers what you will see there.
Causes, most likely first
The subnets and the security group belong to different VPCs
Look up the VPC id of every subnet and every security group in the configuration and check they match. This is the most common cause and the least obvious, because each identifier is individually valid — they simply cannot be combined. A security group from a default VPC alongside subnets from an application VPC is the classic pairing.
The identifier is from another region or account
Check the region the deployment targets against where the subnet actually lives. Subnet and security group IDs carry no region in them, so one copied from another environment looks entirely plausible and refers to nothing here.
The resource was deleted or replaced
Describe the subnet and security group directly. Infrastructure recreated by another team, or a stack rebuilt without updating a hardcoded value elsewhere, leaves references to identifiers that no longer exist.
The subnet has no free IP addresses
Check AvailableIpAddressCount on each subnet. A related exception — SubnetIPAddressLimitReachedException — is raised when the subnet is valid but full, and it reads similarly enough to be mistaken for a bad identifier when it is really a capacity problem.
The identifier is a name rather than an ID
Check the value's shape. Lambda wants subnet-0abc… and sg-0abc…. A security group name works in some EC2 APIs and not here, and a Terraform reference resolving to .name rather than .id produces a value that is not an identifier at all.
Fixes
Check every identifier resolves to the same VPC
This one command answers the question that causes most of these, and it answers it before a deploy rather than after.
bashaws ec2 describe-subnets --subnet-ids subnet-0abc subnet-0def \
--query 'Subnets[].{Subnet:SubnetId,Vpc:VpcId,AZ:AvailabilityZone,Free:AvailableIpAddressCount}'
aws ec2 describe-security-groups --group-ids sg-0abc \
--query 'SecurityGroups[].{Group:GroupId,Vpc:VpcId}'
Reference the resources rather than hardcoding their IDs
In CloudFormation, SAM or Terraform, referring to the subnet and security group resources makes it impossible to point at something deleted, and keeps the VPC consistent by construction.
yamlResources:
OrdersFunction:
Type: AWS::Serverless::Function
Properties:
VpcConfig:
SubnetIds:
- !Ref PrivateSubnetA
- !Ref PrivateSubnetB
SecurityGroupIds:
- !Ref LambdaSecurityGroup
Distinguish "not valid" from "full" before changing the ID
SubnetIPAddressLimitReachedException means the subnet is correct and out of addresses. The fix there is a larger CIDR or additional subnets, not a different identifier — and swapping the ID because the messages read alike wastes a deploy cycle.
bashaws ec2 describe-subnets --subnet-ids subnet-0abc \
--query 'Subnets[0].{Cidr:CidrBlock,Free:AvailableIpAddressCount}'
Use the ID attribute, not the name
Where a template is generating the value, check it resolves to an identifier. Security groups in particular have both, and only one of them works here.
hclresource "aws_lambda_function" "orders" {
vpc_config {
subnet_ids = [for s in aws_subnet.private : s.id]
security_group_ids = [aws_security_group.lambda.id]
}
}
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- AWS Lambda Developer Guide — Connecting outbound networking to resources in a VPC
- AWS Lambda Developer Guide — Troubleshoot networking issues
This one happens before there are any logs.
LogStitch reads CloudWatch, and a deployment that fails never writes to it — so this is not an error it can find for you. Once the function deploys and starts running, the free web stitcher groups its invocations in your browser, and the Mac app does the same across every function in your account.