
Photo by Greece-China News on Pexels
To delete a default VPC safely, find it with DescribeVpcs and the is-default filter in each Region, confirm nothing uses it (no network interfaces, extra security groups, endpoints or peering), then delete its subnets, detach and delete its internet gateway, and call DeleteVpc. If you need it back, CreateDefaultVpc creates a new one.
Every AWS account starts with a default VPC in each Region: public subnets, an internet gateway and a route to it. That’s convenient for a first EC2 instance and a risk for everything after, because a launch that doesn’t name a subnet lands in a public one. Most teams build their own VPCs and never touch the default ones again, which leaves up to one idle, internet-connected network per Region.
This example is for engineers tidying an account before a security review. You get a TypeScript script for the AWS SDK for JavaScript v3 that finds default VPCs in every Region, shows what still uses each one and deletes only the empty ones, and only with --apply. It fits with the other AWS SDK v3 security and cleanup examples.
What’s in a default VPC?
AWS creates the same set of components in every Region. According to the Amazon VPC guide to working with default VPCs and its component list, deleting the VPC removes some of them automatically, and the rest must go first:
| Component | Default setting | When you delete the VPC |
|---|---|---|
| VPC | 172.31.0.0/16 |
Deleted last |
| Default subnets | One /20 per Availability Zone, public IPv4 on launch |
Delete first |
| Internet gateway | Attached, with 0.0.0.0/0 routed to it |
Detach, then delete |
| Main route table | Local route plus the internet route | Deleted with the VPC |
| Default security group | Associated with the VPC | Deleted with the VPC |
| Default network ACL | Associated with the VPC | Deleted with the VPC |
| DHCP options set | The account’s default set | Not deleted; it isn’t owned by the VPC |
The console’s Delete VPC action removes subnets and gateways for you. The API doesn’t: DeleteVpc fails with DependencyViolation until you’ve removed everything except the default security group, main route table and default network ACL.
Should you delete the default VPC?
Delete it if nothing runs there and you always launch into your own VPCs. The payoff is fewer ways to create a public instance by accident: a default subnet assigns a public IPv4 address on launch, and the route table already points at the internet. The script to find EC2 instances with public IP addresses usually shows a few that ended up there this way.
Know the trade-off before you run --apply:
- Without a default VPC, every launch must name a subnet. AWS’s documentation says that if you have no other VPC, you must create one with a subnet in at least one Availability Zone before launching instances.
- Tools and tutorials that assume a default VPC fail until you pass a VPC or subnet explicitly.
- You can’t restore a deleted default VPC.
CreateDefaultVpcbuilds a new one with a new ID, its subnet CIDR blocks may map to different Availability Zones, and you can’t mark an existing VPC as the default.
Keep it, but block it: if you’d rather not delete default VPCs, VPC Block Public Access can block traffic to and from internet gateways in the account. To stop anyone from recreating a default VPC after deletion, deny ec2:CreateDefaultVpc in an IAM policy or service control policy. For default VPCs you keep, the script to find VPCs without flow logs makes sure their traffic is recorded.
What does the script do?
- Lists Regions
--regions allcallsDescribeRegions, which returns the Regions enabled for the account; or pass a comma-separated list. - Finds the default VPC
DescribeVpcswith theis-defaultfilter set totrue. A Region can have one or none. - Looks for anything that uses itNetwork interfaces (instances, load balancers, NAT gateways, Lambda functions and RDS all create them), security groups other than
default, custom route tables and network ACLs, VPC endpoints including gateway endpoints, and active peering connections. - Reports
in use: keepwith the reasons, orempty: would delete. - Deletes only empty ones with
--applyDefault subnets, then the internet gateway (detach and delete), then the VPC. AnyDependencyViolationis reported per Region instead of stopping the run.
Prerequisites
- Node.js 18 or later, npm and
tsx, plus@aws-sdk/client-ec2. - A profile the SDK can resolve; the guide to AWS SDK v3 credential providers such as fromIni and fromSSO explains the lookup order.
- For
--apply: agreement from whoever owns the account, because nothing in the API marks a VPC as “needed by a tutorial next week”.
Which IAM permissions does it need?
The first statement is enough for the report; describe actions use "Resource": "*". Attach the second only to the role that runs --apply.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReportDefaultVpcs",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"ec2:DescribeVpcs",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeSecurityGroups",
"ec2:DescribeRouteTables",
"ec2:DescribeNetworkAcls",
"ec2:DescribeVpcEndpoints",
"ec2:DescribeVpcPeeringConnections",
"ec2:DescribeSubnets",
"ec2:DescribeInternetGateways"
],
"Resource": "*"
},
{
"Sid": "DeleteEmptyDefaultVpcsOnlyWithApply",
"Effect": "Allow",
"Action": [
"ec2:DeleteSubnet",
"ec2:DetachInternetGateway",
"ec2:DeleteInternetGateway",
"ec2:DeleteVpc"
],
"Resource": "*"
}
]
}
The IAM policy generator for TypeScript AWS SDK code can derive this list from the script, and the guide to review a generated IAM policy for least privilege shows how to narrow the delete statement with conditions.
The full script to find and delete default VPCs
// find-default-vpcs.ts
// Finds the default VPC in each Region and checks whether anything uses it: network interfaces,
// extra security groups, custom route tables and network ACLs, VPC endpoints and peering connections.
// Report only by default. With --apply it deletes EMPTY default VPCs in dependency order:
// detach and delete the internet gateway, delete the default subnets, then delete the VPC.
// Usage: npx tsx find-default-vpcs.ts [--regions all | us-east-1,eu-west-1] [--apply]
import {
DeleteInternetGatewayCommand,
DeleteSubnetCommand,
DeleteVpcCommand,
DescribeRegionsCommand,
DescribeVpcsCommand,
DetachInternetGatewayCommand,
EC2Client,
paginateDescribeInternetGateways,
paginateDescribeNetworkAcls,
paginateDescribeNetworkInterfaces,
paginateDescribeRouteTables,
paginateDescribeSecurityGroups,
paginateDescribeSubnets,
paginateDescribeVpcEndpoints,
paginateDescribeVpcPeeringConnections,
type Filter,
} from "@aws-sdk/client-ec2";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const apply = args.includes("--apply");
interface Row {
Region: string;
Vpc: string;
ENIs: number;
Blockers: string; // what keeps the VPC from being "empty"
Status: string;
}
async function listRegions(): Promise<string[]> {
const wanted = flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1";
if (wanted !== "all") return wanted.split(",").map((r) => r.trim()).filter(Boolean);
// Regions enabled for this account (opt-in Regions you haven't enabled are left out).
const { Regions = [] } = await new EC2Client({ region: "us-east-1" }).send(new DescribeRegionsCommand({}));
return Regions.map((r) => r.RegionName ?? "").filter(Boolean).sort();
}
async function blockersFor(ec2: EC2Client, vpcId: string): Promise<{ enis: number; blockers: string[] }> {
const byVpc: Filter[] = [{ Name: "vpc-id", Values: [vpcId] }];
const client = { client: ec2 };
let enis = 0, extraSgs = 0, customRts = 0, customAcls = 0, endpoints = 0, peerings = 0;
for await (const p of paginateDescribeNetworkInterfaces(client, { Filters: byVpc })) enis += p.NetworkInterfaces?.length ?? 0;
for await (const p of paginateDescribeSecurityGroups(client, { Filters: byVpc })) {
extraSgs += (p.SecurityGroups ?? []).filter((g) => g.GroupName !== "default").length;
}
for await (const p of paginateDescribeRouteTables(client, { Filters: byVpc })) {
customRts += (p.RouteTables ?? []).filter((t) => !(t.Associations ?? []).some((a) => a.Main)).length;
}
for await (const p of paginateDescribeNetworkAcls(client, { Filters: byVpc })) {
customAcls += (p.NetworkAcls ?? []).filter((a) => !a.IsDefault).length;
}
for await (const p of paginateDescribeVpcEndpoints(client, { Filters: byVpc })) endpoints += p.VpcEndpoints?.length ?? 0;
const peeringFilters: Filter[] = [{ Name: "status-code", Values: ["active", "pending-acceptance", "provisioning"] }];
for (const side of ["requester-vpc-info.vpc-id", "accepter-vpc-info.vpc-id"]) {
const Filters = [...peeringFilters, { Name: side, Values: [vpcId] }];
for await (const p of paginateDescribeVpcPeeringConnections(client, { Filters })) peerings += p.VpcPeeringConnections?.length ?? 0;
}
const blockers = [
enis && `${enis} ENIs`,
extraSgs && `${extraSgs} extra SGs`,
customRts && `${customRts} custom route tables`,
customAcls && `${customAcls} custom NACLs`,
endpoints && `${endpoints} endpoints`,
peerings && `${peerings} peerings`,
].filter((b): b is string => Boolean(b));
return { enis, blockers };
}
async function deleteDefaultVpc(ec2: EC2Client, vpcId: string): Promise<void> {
const client = { client: ec2 };
// 1. Subnets (the default VPC has one default subnet per Availability Zone).
for await (const page of paginateDescribeSubnets(client, { Filters: [{ Name: "vpc-id", Values: [vpcId] }] })) {
for (const s of page.Subnets ?? []) await ec2.send(new DeleteSubnetCommand({ SubnetId: s.SubnetId }));
}
// 2. Internet gateway: detach, then delete.
for await (const page of paginateDescribeInternetGateways(client, { Filters: [{ Name: "attachment.vpc-id", Values: [vpcId] }] })) {
for (const igw of page.InternetGateways ?? []) {
await ec2.send(new DetachInternetGatewayCommand({ InternetGatewayId: igw.InternetGatewayId, VpcId: vpcId }));
await ec2.send(new DeleteInternetGatewayCommand({ InternetGatewayId: igw.InternetGatewayId }));
}
}
// 3. The VPC. Its default security group, main route table and default network ACL go with it.
await ec2.send(new DeleteVpcCommand({ VpcId: vpcId }));
}
async function scanRegion(region: string): Promise<Row[]> {
const ec2 = new EC2Client({ region });
const { Vpcs = [] } = await ec2.send(new DescribeVpcsCommand({ Filters: [{ Name: "is-default", Values: ["true"] }] }));
const rows: Row[] = [];
for (const vpc of Vpcs) {
const vpcId = vpc.VpcId ?? "";
const { enis, blockers } = await blockersFor(ec2, vpcId);
let status = blockers.length ? "in use: keep" : apply ? "deleting" : "empty: would delete";
if (!blockers.length && apply) {
try {
await deleteDefaultVpc(ec2, vpcId);
status = "deleted";
} catch (err) {
status = `failed: ${err instanceof Error ? err.name : String(err)}`;
}
}
rows.push({ Region: region, Vpc: vpcId, ENIs: enis, Blockers: blockers.join(", ") || "none", Status: status });
}
if (!Vpcs.length) rows.push({ Region: region, Vpc: "(no default VPC)", ENIs: 0, Blockers: "", Status: "-" });
return rows;
}
async function main(): Promise<void> {
const rows: Row[] = [];
for (const region of await listRegions()) rows.push(...(await scanRegion(region)));
console.table(rows);
const empty = rows.filter((r) => r.Status.startsWith("empty") || r.Status === "deleted").length;
const used = rows.filter((r) => r.Status.startsWith("in use")).length;
console.log(`${empty} empty default VPCs, ${used} in use`);
if (!apply) console.log("Dry run: nothing was deleted. Re-run with --apply to delete the empty ones.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-ec2
npm install --save-dev tsx typescript
# Report every enabled Region
AWS_PROFILE=readonly npx tsx find-default-vpcs.ts --regions all
# Delete the empty ones in two Regions
AWS_PROFILE=netadmin npx tsx find-default-vpcs.ts --regions eu-north-1,ap-southeast-2 --apply
# Changed your mind? A new default VPC, per Region
aws ec2 create-default-vpc --region eu-north-1
Sample output
┌─────────┬──────────────────┬────────────────────┬──────┬─────────────────────────────────────┬───────────────────────┐
│ (index) │ Region │ Vpc │ ENIs │ Blockers │ Status │
├─────────┼──────────────────┼────────────────────┼──────┼─────────────────────────────────────┼───────────────────────┤
│ 0 │ 'ap-southeast-2' │ 'vpc-0a1b2c3d' │ 0 │ 'none' │ 'empty: would delete' │
│ 1 │ 'eu-north-1' │ 'vpc-0b2c3d4e' │ 0 │ 'none' │ 'empty: would delete' │
│ 2 │ 'eu-west-1' │ 'vpc-0c3d4e5f' │ 3 │ '3 ENIs, 1 extra SGs' │ 'in use: keep' │
│ 3 │ 'us-east-1' │ 'vpc-0d4e5f60' │ 0 │ '1 endpoints' │ 'in use: keep' │
│ 4 │ 'us-west-2' │ '(no default VPC)' │ 0 │ '' │ '-' │
└─────────┴──────────────────┴────────────────────┴──────┴─────────────────────────────────────┴───────────────────────┘
2 empty default VPCs, 2 in use
Dry run: nothing was deleted. Re-run with --apply to delete the empty ones.
Output shortened to five Regions; IDs are illustrative. The us-east-1 VPC has no network interfaces but does have an endpoint, most likely an S3 gateway endpoint, which has no ENI; the script keeps it so you can check who created it. The eu-west-1 VPC runs something: the script to find unattached elastic network interfaces tells you whether those three ENIs are leftovers or live.
Troubleshooting
DependencyViolationafter the subnets are gone. Something the script doesn’t check is attached, such as a virtual private gateway or an egress-only internet gateway. Delete it, then re-run; the empty subnets are already deleted, so the next run goes straight to the gateway and VPC.- ENIs remain after you deleted the resource. Some services release their network interfaces after a delay. Re-run later, or look at each ENI’s description and requester.
- “Extra SGs” but no ENIs. Security groups that nothing uses still block
DeleteVpc. The script to find unused security groups in your AWS account lists them for deletion first. UnauthorizedOperation. A describe or delete action is missing from the profile; the steps to troubleshoot AWS IAM access denied errors decode the message.
Ask ChatWithCloud instead
For a single Region, ask ChatWithCloud “Does the default VPC in eu-north-1 have any network interfaces or endpoints?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and explains the answer, as described on the page about how ChatWithCloud runs AWS SDK code on your machine. It works with one profile and Region per session, and it runs generated code without asking first, so a delete question could delete. Use a read-only profile, as the ChatWithCloud security model recommends, and keep the script for the deletion itself.
Frequently asked questions
Is it safe to delete the default VPC?
Yes, if nothing uses it. AWS lets you delete it like any other VPC, and you can create a new default VPC later. Check for network interfaces, endpoints and peering first, because deleting removes the network those resources depend on.
How do I delete a default VPC with the AWS CLI?
Delete each default subnet with aws ec2 delete-subnet, detach and delete the internet gateway with detach-internet-gateway and delete-internet-gateway, then run aws ec2 delete-vpc --vpc-id vpc-.... The default security group, route table and network ACL go with the VPC.
Can I restore a deleted default VPC?
No. CreateDefaultVpc creates a new default VPC with the standard components, but not the old one, and only if the Region has no default VPC.
What happens if I launch an EC2 instance after deleting the default VPC?
You must specify a subnet in one of your VPCs in the launch request. If the account has no other VPC in that Region, create one with at least one subnet first, or run aws ec2 create-default-vpc.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud