I wanted to write down what a VPC actually is, in one place. Isolation, routing, connectivity, security — four kinds of decisions that meet on the same packet anyway, and seeing them together feels better for the mental model than four scattered posts.

I kept the scope to what a backend engineer actually decides. Network-engineer details (BGP internals, IPSec ciphers, NAT instance vs. Gateway comparisons) are out.

VPC and Subnet

When you create a VM in the cloud, it lands in some network. That network is a VPC. AWS, GCP, and Alibaba call it VPC; Azure calls it VNet. The abstraction is the same. It’s an isolated virtual network with its own private IP space, built on top of shared cloud infrastructure with SDN (Software-Defined Networking).

A public cloud is multi-tenant by definition. Many customers’ workloads run on the same physical hardware. Without isolation, traffic leaks and IP addresses collide. A VPC gives each tenant something close to their own data center.

CIDR

The first thing you pick when creating a VPC is the CIDR (Classless Inter-Domain Routing) block. A range like 10.0.0.0/16 becomes the address space for resources inside the VPC.

The standard choice is one of the RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). They aren’t routable on the public internet, so they don’t collide with anything outside, and other VPCs can reuse the same range freely.

The catch shows up when you eventually want to connect VPCs. Two VPCs both using 10.0.0.0/16 will produce ambiguous routing the moment you set up Peering or a Transit Gateway. Allocating IP ranges by organization up front is the safer move.

The prefix length is also hard to change after the fact. Reserve enough room for growth.

Subnet

A Subnet is a smaller block carved out of the VPC’s IP space, usually aligned to an availability zone (AZ).

AZs are physically separated data center groups, so placing Subnets per AZ is the baseline for availability. When one AZ has an outage, Subnets in other AZs are unaffected.

For isolation, the Subnet’s role is less about separation and more about policy scope. Routing rules and security rules attach at the Subnet level. The “Public Subnet” / “Private Subnet” labels themselves are a consequence of routing, which is the next section.

There’s also a tenancy option that takes isolation down to the physical hardware (dedicated tenancy). It rarely comes up in a backend engineer’s decisions.

Routing

The Route Table decides where a packet inside a VPC goes. It attaches to a VPC or Subnet and pairs destination CIDRs with next hops (targets).

Matching is longest prefix match. The more specific CIDR wins. Given 0.0.0.0/0 and 10.0.5.0/24 in the same table, a packet to 10.0.5.42 takes the /24 rule.

A Local route is added automatically at VPC creation, covering the VPC’s full CIDR. That’s why resources inside the same VPC can talk to each other without any extra setup. The Local route can’t be removed.

IGW and NAT

External traffic uses two distinct kinds of gateways.

The Internet Gateway(IGW) is the bidirectional door between a VPC and the internet. One IGW per VPC. For a resource to be reachable from outside, two conditions must both hold. It needs a Public IP (or Elastic IP), and the Subnet’s Route Table must have a default route pointing at the IGW. Either alone is not enough.

The NAT Gateway (Network Address Translation) is outbound-only. Use it when resources in a Private Subnet need to reach out to the internet but must not be reachable from outside. The NAT itself sits in a Public Subnet because the traffic still leaves through the IGW. A Private Subnet’s Route Table points its default route at the NAT, and the NAT rewrites the source to its own Public IP on the way out.

Connections initiated from outside don’t pass through the NAT, so inbound protection comes along for free. The cost side is real though. NAT Gateways are billed per hour plus per byte processed, which adds up fast for outbound-heavy workloads.

What Public and Private Subnets Really Are

This is where backend engineers trip up most often.

A “Public Subnet” or “Private Subnet” is not a property of the Subnet. It’s a consequence of where the Route Table’s default route points.

  • Public Subnet: default route points to the IGW
  • Private Subnet: default route points to the NAT, or there’s no default route at all
flowchart LR
    subgraph Public ["Public Subnet"]
        VM_P["VM (Public IP)"] -.-> RT_P["Route Table
0.0.0.0/0 → IGW"] end subgraph Private ["Private Subnet"] VM_R["VM"] -.-> RT_R["Route Table
0.0.0.0/0 → NAT"] end RT_P --> IGW["IGW"] RT_R --> NAT["NAT Gateway"] NAT --> IGW

Two Subnets in the same VPC are simply attached to different Route Tables. The Subnet itself doesn’t carry a Public/Private flag.

Once that definition clicks, common traps fall out naturally. “I attached a Public IP but it’s still unreachable from outside” usually means the Subnet’s Route Table doesn’t have a default route to the IGW. Public IP alone isn’t sufficient; the routing condition has to be there too.

Connecting Outside the VPC

Once routing inside the VPC is settled, the next question is how to reach things outside it. Other VPCs, on-prem data centers, external SaaS. Each has its own mechanism, and the choice ends up shaping the topology and cost of the system.

VPC Peering

The simplest option. Two VPCs are connected directly so each side can reach the other’s private IP space. It works across regions and accounts.

The limit is no transitive routing. If A-B and B-C are both Peered, A still can’t reach C. Both sides need a direct Peering. Connection count grows as N(N-1)/2 with the number of VPCs, so Peering only scales for a small handful.

Transit Gateway

Once VPC count grows, the Peering mesh becomes painful fast. Transit Gateway is a central hub. Many VPCs attach as spokes, and traffic between spokes flows transitively through the hub. Connection count grows linearly.

flowchart LR
    subgraph Peering ["Peering: mesh"]
        VA["VPC A"] --- VB["VPC B"]
        VB --- VC["VPC C"]
        VA --- VC
        VC --- VD["VPC D"]
        VA --- VD
        VB --- VD
    end
    subgraph Transit ["Transit Gateway: hub-spoke"]
        TGW(("TGW"))
        TVA["VPC A"] --- TGW
        TVB["VPC B"] --- TGW
        TVC["VPC C"] --- TGW
        TVD["VPC D"] --- TGW
    end

The cost model differs. There’s a per-hour attachment fee plus per-byte processing. Pricier than Peering at small scale, but the crossover comes quickly as N grows.

Site-to-Site VPN

The first option to consider when tying a VPC to an on-prem data center. An IPSec tunnel over the public internet logically connects the two networks. Static and dynamic (BGP) routing are both available.

Since the medium is the public internet, bandwidth and latency are variable. Dedicated alternatives (Direct Connect, Cloud Interconnect) exist when stability matters.

PrivateLink exposes a single service as an endpoint. It doesn’t connect two networks at the IP level the way the previous three do.

The provider VPC creates an endpoint, and the consumer VPC sees it as an ENI (Elastic Network Interface) inside its own network. The two VPCs’ IP spaces can be anything. There’s no CIDR coupling, because the link is per-endpoint, not per-network.

It’s also unidirectional. The provider exposes a service that the consumer calls; the reverse direction needs its own endpoint. Common cases include private SaaS exposure and cloud managed services that want a VPC ingress point.

Comparison

MechanismTopologyTransitiveCost modelMain use case
VPC Peering1:1 meshRelatively cheap (per byte)Small number of VPCs, direct
Transit Gatewayhub-spokePer hour + per byteMany VPCs, separable routing domains
Site-to-Site VPNsite ↔ VPC tunnel(✅ via BGP)Per hour + per byteOn-prem ↔ VPC
PrivateLinkservice endpoint(N/A)Per endpoint + per byteService-level exposure, CIDR-agnostic

The first three all share one trap: if two VPCs’ CIDRs overlap, the packet’s destination becomes ambiguous and routing breaks. If overlap already exists, PrivateLink is effectively the only way out. It exposes services by endpoint rather than routing by IP, so identical CIDRs on both sides are fine.

Security

SGs and NACLs look like the same kind of firewall rule, but their scope, evaluation, and statefulness all differ. From a backend perspective, SG is the one you touch often; the NACL is usually an extra layer the infra/security team configures at the Subnet boundary.

Security Group

A Security Group (SG) attaches at the instance or ENI level. An instance can have multiple SGs; their rules are evaluated as a union.

The defining trait is stateful. Once a connection is allowed, the return traffic is allowed automatically. You don’t need to mirror an inbound rule on the outbound side for responses.

SGs are allow-only. There are no deny rules; only what you explicitly allow gets through. The defaults are all outbound allowed, all inbound blocked. A fresh SG blocks every inbound, and you open ports and source ranges one by one.

A nice feature: the source/destination can be another SG’s ID, not just a CIDR. You can express “only allow resources tagged with this SG” directly, so rules survive when instance IPs change. This is what backend engineers reach for most often.

NACL

A NACL (Network ACL) attaches at the Subnet level. One NACL per Subnet, and every resource in the Subnet inherits its effect.

NACLs are stateless. Inbound and outbound rules are fully independent. Even response traffic for an allowed flow is blocked unless the opposite direction explicitly allows it. The return traffic that SGs auto-allow needs its own rule on a NACL.

NACLs also let you express both allow and deny, which is why they exist alongside SGs. When you need an explicit block, NACL is the layer for it.

The Stateless Trap

You allow outbound on the NACL but responses are blocked. Invisible while you’re only using SGs, this appears the moment a NACL enters the picture.

Response traffic generally comes back on ephemeral ports (1024-65535). If the NACL’s inbound rules don’t cover that range, responses are dropped. When you own a NACL, allowing ephemeral port ranges in both directions is the standard fix.

Comparison

AspectSecurity GroupNACL
ScopeInstance / ENISubnet
StatefulnessStateful (return auto-allow)Stateless (return explicit)
Rule kindsAllow onlyAllow + deny
EvaluationUnion of all rulesBy rule number, first match
DefaultsInbound blocked, outbound allowed(depends on default vs. custom)
SourceCIDR + other SG IDsCIDR only

Debugging Checklist

When traffic isn’t reaching an instance, having a fixed order to check builds the mental model. Top to bottom usually finds the cause.

  1. SG inbound. Most common. Are source IP/SG and port allowed?
  2. SG outbound. When the instance initiates the connection itself. Default is fully open, but if customized, check.
  3. Subnet Route Table. Does the default route point at the IGW (if ingress needed) or NAT (if outbound needed)? Local route covers same-VPC traffic automatically.
  4. Public IP / Elastic IP. If external inbound is needed, is the IP attached? Even when attached, the routing condition still has to be satisfied.
  5. NACL. Usually an infra-team layer, but its stateless nature means ephemeral ports can be missing. If SG clearly allows but responses don’t come back, suspect this.
  6. CIDR overlap. In environments connected via Peering or TGW, traffic failures between VPCs can come from overlapping CIDRs.

Vendor Naming

I used AWS names throughout. Here’s how the other vendors map onto the same abstractions.

ConceptAWSGCPAzureAlibaba Cloud
Virtual private networkVPCVPCVirtual Network (VNet)VPC
SubnetSubnetSubnetSubnetVSwitch
External gatewayInternet Gateway(Routes next hop)Public IP + NSGInternet Gateway
Outbound-only exitNAT GatewayCloud NATNAT GatewayNAT Gateway
1:1 VPC connectionVPC PeeringVPC Network PeeringVNet PeeringVPC Peering
Hub-spoke multi-VPCTransit GatewayNetwork Connectivity CenterVirtual WANCEN
On-prem IPSecSite-to-Site VPNCloud VPNVPN GatewayVPN Gateway
Service-level exposurePrivateLinkPrivate Service ConnectPrivate LinkPrivateLink
Instance-level statefulSecurity GroupFirewall Rule (tag-based)NSG (NIC-applied)Security Group
Subnet-level statelessNetwork ACL(no direct equivalent)NSG (Subnet-applied)Network ACL

The names shift slightly, but the abstractions are nearly the same. GCP rolls SG/NACL together into Firewall Rules, and Azure uses a single NSG resource that can attach at either the NIC or Subnet level. Those are the bigger structural differences.


The four abstractions can be learned separately, but they end up meeting on the same packet. Once they’re in one place, four things show up together whenever I’m debugging.