Subnet

A subnet is a range of IP addresses inside a cloud network, bound to one zone and to a route table that decides whether machines in it reach the internet.

A subnet is a range of IP addresses carved out of a larger cloud network, attached to one availability zone and to a route table that decides its reachability. Every machine launched into the subnet takes one address from that range and inherits those routes, so the subnet sets both how many machines can run at once and whether they can reach the public internet.

Both of those properties matter for GitHub Actions. A self hosted runner is a machine, the machine needs an address, and the job running on it needs to reach GitHub, package registries, and internal services.

Definition

A subnet is a subdivision of a virtual network's address space, written in CIDR notation. Three things travel with it: a range of addresses, a placement inside the provider's failure domains, and a set of routes.

The wording differs across providers and the shape holds.

ProviderWhat a subnet is scoped toReserved addresses per IPv4 rangeSmallest IPv4 range
AWSOne Availability Zone, and it cannot span zones5: the first four and the last/28
Google CloudOne region4: the first two, the second to last, and the last/29
AzureA virtual network, which lives in one region5: the first four and the last/29

Sources: Subnets for your VPC and subnet CIDR blocks on AWS, subnets on Google Cloud, and the virtual network FAQ on Azure, all checked on 2026-08-13.

Reserved addresses and usable capacity

The reserved addresses are the part people forget when sizing a range. AWS documents that the first four IP addresses and the last IP address in each subnet CIDR block cannot be assigned to a resource: the network address, the VPC router, the DNS server address, one held for future use, and the broadcast address. AWS allows a subnet between a /28 and a /16 netmask.

That arithmetic decides how many machines can exist in the subnet at the same time.

Subnet CIDRTotal addressesReserved by AWSUsableSingle interface machines that fit
/281651111
/266455959
/242565251251
/221,02451,0191,019
/204,09654,0914,091

A machine with a second network interface consumes a second address, and load balancers, endpoints, and container networking modes that give each pod an address draw from the same pool. Running out of addresses shows up as a launch failure rather than a slow job, so the ceiling is worth checking before a fleet is scaled up.

Routes decide reachability

AWS documents that each subnet must be associated with a route table, which specifies the allowed routes for outbound traffic leaving the subnet, and that every new subnet is associated with the VPC main route table until the association is changed. The subnet type follows from that table:

  • Public subnet: the route table has a direct route to an internet gateway, and resources in it can reach the public internet.
  • Private subnet: no direct route to an internet gateway, so resources need a NAT device to reach the public internet.
  • Isolated subnet: no routes to destinations outside the virtual network at all.

Azure follows the same pattern with system routes plus optional user defined route tables attached to a subnet. Google Cloud differs: it creates a subnet route for each range automatically and applies other routes at the network level, selecting them with instance tags rather than binding a table to one subnet.

Address assignment is separate from routing. On AWS, whether a network interface created in the subnet gets a public IPv4 address is a modifiable subnet attribute, and a launch can override it. A machine with no public address in a subnet whose route table points at a NAT device still reaches the internet outbound, and nothing on the internet can open a connection to it.

Zones, regions, and how many subnets you need

Because an AWS subnet sits in exactly one Availability Zone, spreading a workload across three zones means three subnets, each with its own CIDR block and its own route table association. Capacity for a given instance type is also decided per zone, so a fleet pinned to one subnet is pinned to the capacity of one zone.

Example

A self hosted GitHub Actions runner does not need an inbound path. The runner agent opens an outbound HTTPS connection to GitHub and waits for work on it, so a private subnet whose route table sends outbound traffic through a gateway is enough, and no machine in the fleet needs a public address.

The route table on that private subnet has two entries, plus a third when a gateway endpoint exists:

DestinationTargetEffect
10.0.0.0/16localTraffic inside the virtual network stays inside it
0.0.0.0/0NAT gateway in a public subnetOutbound internet traffic leaves through the gateway's address
Storage service prefix listGateway endpointTraffic to that service follows the endpoint route instead

A job scheduled onto a runner in that subnet sees the result directly:

name: integration
on:
  pull_request:

jobs:
  test:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4

      - name: Show which addresses the job is using
        run: |
          hostname -I
          curl -s https://api.ipify.org

      - name: Reach an internal service
        run: curl -sf http://db.internal:5432 || true

      - run: ./scripts/integration-test.sh

hostname -I prints an address from the subnet's CIDR block, such as 10.0.12.37. The curl to an echo service prints the NAT gateway's public address instead, because that is the address the outbound traffic is translated to. Every runner in the subnet presents that same source address, which is what makes an allowlist on an internal database or a partner API practical for a fleet whose machines are created and destroyed constantly.

Sizing the range for this fleet is the arithmetic from the table above. A team that wants 250 runners in one zone at peak needs at least 250 usable addresses there, so a /24 fits and a /25 does not. The AWS BYOC configuration checklist asks for the same shape: at least one public and one private subnet with internet connectivity, a NAT gateway on the private subnets, three public and three private subnets across separate availability zones, and a minimum of 250 addresses per subnet.

Two failure modes follow from getting this wrong. A range that is too small stops launching machines at peak, and jobs queue while the fleet looks healthy. A private subnet with no route to a NAT device launches machines that come up, fail to register with GitHub, and are torn down, which reads as a provisioning problem rather than a routing one.

FAQ

What is a subnet in a cloud network?

A subnet is a range of IP addresses carved out of a larger virtual network. Every machine launched into it takes an address from that range, and the routes attached to the subnet decide where traffic leaving those machines is allowed to go. AWS scopes a subnet to a single Availability Zone, while Google Cloud subnets are regional.

What is the difference between a public subnet and a private subnet?

The difference is one route. AWS documents a public subnet as one whose route table has a direct route to an internet gateway, and a private subnet as one without that route, where resources need a NAT device to reach the public internet. Nothing about the address range itself changes.

How many IP addresses does a subnet need for GitHub Actions runners?

One address per running machine, plus the addresses the provider reserves. AWS reserves the first four and the last address in every subnet CIDR block, so a /24 offers 251 usable addresses and a /28 offers 11. A fleet that scales to 250 concurrent runners does not fit in a /25.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.