⚖️ Azure Load Balancer: Set Up a Public Load Balancer for Two Servers, and What Goes Wrong
What Azure Load Balancer actually is (a layer 4 pass-through, not a web proxy), its parts, how it picks a server, how health probes work, the exact commands to build a public one for two servers, the outbound traffic surprise, idle timeouts, the neighbours (Application Gateway, Front Door, Traffic Manager) and what it costs
💡 How this page was written. From Microsoft's documentation, read on 9 October 2026: the Load Balancer overview, components, health probes, distribution modes, SKUs, outbound connections (SNAT), TCP reset and idle timeout pages, the public load balancer CLI quickstart (last updated 27 August 2026), the "Load balancing options" architecture guide, and the Load Balancer pricing page. Nothing here was run on an Azure account (there is none to use, see the account tutorial), so every Azure command is copied from Microsoft's quickstart and not tested. Three small things were run on a laptop and are labelled: three Python models (how a hash spreads connections, how long a dead server keeps getting traffic, how many outbound ports each server gets) and a cloud-init file validated offline. The models show the idea, they are not Azure's code.
What it is, in plain words
Azure Load Balancer spreads incoming network connections over a group of servers (virtual machines, or a virtual machine scale set) and stops sending them to servers that have stopped answering.
The one fact that explains nearly everything else: it works at layer 4. It understands TCP and UDP and addresses and port numbers, and nothing about HTTP. It does not read your web requests, terminate TLS, look at URLs or add headers. Microsoft calls this pass-through load balancing: the client's connection goes through to the chosen server, and the load balancer never becomes a party to the conversation. If you need URL routing, TLS offload or a web application firewall, that is a different service (see "The neighbours" below).
- It comes in two types. A public load balancer has a public IP as its front door and takes traffic from the internet. An internal (private) one has a private IP and only takes traffic from inside a virtual network or from on-premises over a VPN or ExpressRoute link.
- It comes in SKUs. Standard is the one to use. A Gateway SKU exists for third-party network appliances (not covered). Basic was retired on 30 September 2025: older tutorials that say --sku Basic describe something Microsoft no longer offers, and Microsoft's page tells existing users to upgrade.
- Standard has a 99.99% SLA (from Microsoft's SKU table), supports availability zones, and is closed to inbound traffic by default (below).
The parts
You build a load balancer from five pieces. Learn the names, because the CLI commands follow them:
- Frontend IP configuration: the address clients connect to. A public IP makes a public load balancer; a private IP makes an internal one. A load balancer can have several.
- Backend pool: the group of servers that receive the traffic. Members are added by their network interface or by IP address. A server does not need its own public IP, and a stopped VM can be in the pool. Everything in the pool must be in one virtual network.
- Health probe: the check that decides which servers are alive.
- Load-balancing rule: "traffic arriving at this frontend IP and port goes to this backend pool, port so-and-so, using this health probe". Rules apply to inbound traffic only. A probe is required by a rule.
- Inbound NAT rule: forwards a frontend port to one specific server instead of the pool: the classic use is "port 50001 reaches server 1's SSH". It needs no health probe.
- Outbound rule: controls how the servers' own outbound traffic is translated to a public address (see the outbound section; it is where people get hurt).
There is also HA ports: a rule with protocol All and port 0 on an internal Standard load balancer, which balances every TCP and UDP port at once. It exists for firewalls and other network virtual appliances, not for ordinary web servers.
Closed by default: the part that stops beginners
A Standard load balancer and its public IP are closed to inbound connections until a network security group (NSG) explicitly allows the traffic. If there is no NSG on the subnet or on the server's network interface, the traffic is blocked. (Basic, which is retired, was open by default.) So for a web server behind the load balancer you need all of these:
- a rule on the load balancer (port 80 to the pool),
- a NSG rule allowing port 80 into the servers (priority 200 in Microsoft's example),
- and the servers must actually listen on that port.
The health probe also has to get through. Its traffic comes from one address, 168.63.129.16, which the NSG service tag AzureLoadBalancer stands for, and it is allowed by default. Microsoft says to allow that address in any local firewall on the server too, and warns against blocking it with NSG rules ("an unsupported scenario"). Do not use the address range that contains 168.63.129.16 in your own virtual network.
Which server gets the connection
By default the load balancer uses a hash of five values: source IP, source port, destination IP, destination port and protocol. The same five values always give the same server, so one connection stays on one server. But a new connection from the same client has a new source port, so it can land on a different server. Microsoft's words: the algorithm "provides stickiness only within a transport session".
The alternative is session persistence (also called source IP affinity or client IP affinity):
| Mode | Portal name | REST API value | Hash of |
|---|---|---|---|
| Default | Session persistence: None | Default | 5 values |
| Client IP | Client IP | SourceIP | 2 values: source IP and destination IP |
| Client IP and protocol | Client IP and protocol | SourceIPProtocol | 3 values: plus the protocol |
You can switch between modes without downtime. Two warnings from Microsoft: when servers are added to or removed from the pool, "the distribution of client requests is recomputed" and you "can't depend on new connections from existing clients to end up at the same server"; and source IP affinity "can cause an uneven distribution of traffic", because "clients that run behind proxies might be seen as one unique client". Persistence is a convenience, not a session store: keep your session data somewhere shared.
Here is the idea in a model (SHA-256 of the fields, three servers; Azure's real hash function is not public, so only the properties carry over):
>>> import hashlib, random, collections
>>> backends = ['vm1', 'vm2', 'vm3']
>>> def pick(*fields): return backends[int(hashlib.sha256('|'.join(map(str, fields)).encode()).hexdigest(), 16) % len(backends)]
... # one client, six connections: the source port changes every time
>>> client, vip = '203.0.113.10', '198.51.100.5'
>>> ports = [50211, 50212, 50213, 50214, 50215, 50216]
>>> print('five-tuple:', [pick(client, p, vip, 80, 'tcp') for p in ports])
five-tuple: ['vm3', 'vm3', 'vm2', 'vm2', 'vm2', 'vm1']
>>> print('two-tuple: ', [pick(client, vip) for p in ports])
two-tuple: ['vm2', 'vm2', 'vm2', 'vm2', 'vm2', 'vm2']
>>> print('three-tuple:', [pick(client, vip, 'tcp') for p in ports])
three-tuple: ['vm3', 'vm3', 'vm3', 'vm3', 'vm3', 'vm3']
>>> # 3000 clients, 5 connections each, random source ports
>>> random.seed(7)
>>> clients = ['10.%d.%d.%d' % (random.randrange(256), random.randrange(256), random.randrange(1, 255)) for _ in range(3000)]
>>> flows = [(c, random.randrange(32768, 61000)) for c in clients for _ in range(5)]
>>> print(len(flows), 'flows')
15000 flows
>>> print('five-tuple:', dict(sorted(collections.Counter(pick(c, p, vip, 80, 'tcp') for c, p in flows).items())))
five-tuple: {'vm1': 5084, 'vm2': 5005, 'vm3': 4911}
>>> print('two-tuple: ', dict(sorted(collections.Counter(pick(c, vip) for c, p in flows).items())))
two-tuple: {'vm1': 5095, 'vm2': 4895, 'vm3': 5010}
>>> # the same traffic after a firewall does SNAT: only 2 source addresses are visible
>>> snat = [('192.0.2.1' if sum(map(ord, c)) % 2 else '192.0.2.2', p) for c, p in flows]
>>> print('two-tuple behind SNAT: ', dict(sorted(collections.Counter(pick(c, vip) for c, p in snat).items())))
two-tuple behind SNAT: {'vm1': 15000}
>>> print('five-tuple behind SNAT:', dict(sorted(collections.Counter(pick(c, p, vip, 80, 'tcp') for c, p in snat).items())))
five-tuple behind SNAT: {'vm1': 5052, 'vm2': 5001, 'vm3': 4947}
>>> # a backend is added: how many of the SAME clients move under source IP affinity?
>>> before = {c: pick(c, vip) for c in clients}
>>> backends.append('vm4')
>>> after = {c: pick(c, vip) for c in clients}
>>> moved = sum(before[c] != after[c] for c in clients)
>>> print(moved, 'of', len(clients), 'clients moved to a different backend')
2260 of 3000 clients moved to a different backend
- One client, six connections: the five-value hash spreads them (vm3, vm3, vm2, vm2, vm2, vm1), while the two-value and three-value hashes send all six to the same server.
- With 3,000 different clients, both modes come out even (about 5,000 each of 15,000 flows).
- Put a firewall doing SNAT in front, so that only two source addresses are visible: the five-value hash still spreads the load, but the two-value hash sent everything to one server. (With two addresses it is luck whether they share a server; here they did. Microsoft's note describes the same trap: few visible source addresses means skewed or no distribution.)
- When a fourth server is added, a plain "hash modulo the number of servers" moved 2,260 of 3,000 clients to a different server. That number belongs to my toy model; Microsoft only says the distribution is recomputed.
Health probes
The probe decides who gets new connections. Three kinds on Standard:
- TCP: a normal TCP handshake to the probe port. It fails if the port does not answer within the timeout, or if the server sends a reset.
- HTTP and HTTPS: an HTTP GET for a path you choose. It is up only for a 200 response. Any other code (403, 404, 500) is down, as is no answer. Use these when the port being open is not proof that the application works: your own health page can say "I am draining, send no more".
Settings: protocol, port, interval and threshold (the number of consecutive successes or failures needed to change state). The default interval is 5 seconds in the portal but 15 seconds when you create the probe with the CLI, PowerShell, a template or the API (minimum 5). HTTP/HTTPS probes have a built-in 30-second timeout. A server is probed only while it is running.
Two details worth knowing. For HTTP probes an explicit answer takes effect at once: a 200 marks it up and any other code marks it down immediately, so the threshold counts only when the probe gets no response (timeouts). And when a probe goes down, existing TCP connections are not cut: new connections stop, established ones continue until the application ends them, an idle timeout hits, or the VM shuts down. If all probes are down, Standard keeps existing TCP flows alive too (Microsoft adds the condition "given that a backend pool has more than one backend instance") but sends no new ones, while existing UDP flows end.
How long does a dead server keep getting traffic? Simple arithmetic from the documented rule (a model: failures happen at a random moment, and a server is marked down after threshold consecutive failed probes):
>>> def window(interval, threshold):
... # a failure can happen just after a good probe: the first failed probe comes within one interval,
... # and 'threshold' consecutive failures are needed
... return (threshold - 1) * interval, threshold * interval
...
>>> print('interval threshold marked down after (seconds)')
interval threshold marked down after (seconds)
>>> for interval, threshold in [(5, 1), (5, 2), (5, 3), (15, 2), (15, 3), (30, 2)]:
... lo, hi = window(interval, threshold)
... print(f'{interval:8} {threshold:9} {lo:3} to {hi:3}')
...
5 1 0 to 5
5 2 5 to 10
5 3 10 to 15
15 2 15 to 30
15 3 30 to 45
30 2 30 to 60
With the CLI default of 15 seconds and a threshold of 2, a dead server can still receive new connections for 15 to 30 seconds. With 5 seconds and 2, about 5 to 10. Recovery works the same way in reverse, and Microsoft says that a probe that flaps makes the balancer wait longer before trusting the server again, "an intentional policy".
Limits and hints from the same page: HTTPS probes do not support client-certificate authentication; HTTP probes cannot use host names and cannot probe a handful of ports (19, 21, 25, 70, 110, 119, 143, 220, 993); TCP timestamps can cause throttling that makes probes time out. Microsoft suggests simulating a failure by blocking the probe port with an NSG rule on one server, but the same page also calls blocking the probe address or port with NSG rules "an unsupported scenario" that can take effect late, so treat that as a rough test and prefer stopping the web server. Probe a port that reflects real health, and never make a server answer the probe on behalf of another server (a proxy), or one broken backend will take the front server down with it. The probe definition is only validated in the portal, not by the CLI.
Build one: the commands
This is Microsoft's quickstart for a public Standard load balancer with two servers, in the Azure CLI. It is the sequence (shortened where the quickstart repeats itself; the Bastion, NIC, VM and NAT gateway steps are listed as comments). Not run. It needs CLI 2.0.28 or later; Cloud Shell already has it.
az group create --name load-balancer-cli-rg --location eastus
az network vnet create --resource-group load-balancer-cli-rg --location eastus \
--name lb-vnet --address-prefixes 10.1.0.0/16 \
--subnet-name backend-subnet --subnet-prefixes 10.1.0.0/24
az network public-ip create --resource-group load-balancer-cli-rg \
--name lb-frontend-ip --sku Standard --zone 1 2 3
az network lb create --resource-group load-balancer-cli-rg --name load-balancer \
--sku Standard --public-ip-address lb-frontend-ip \
--frontend-ip-name lb-frontend --backend-pool-name lb-backend-pool
az network lb probe create --resource-group load-balancer-cli-rg --lb-name load-balancer \
--name lb-health-probe --protocol tcp --port 80
az network lb rule create --resource-group load-balancer-cli-rg --lb-name load-balancer \
--name lb-HTTP-rule --protocol tcp --frontend-port 80 --backend-port 80 \
--frontend-ip-name lb-frontend --backend-pool-name lb-backend-pool \
--probe-name lb-health-probe --disable-outbound-snat true \
--idle-timeout 15 --enable-tcp-reset true
az network nsg create --resource-group load-balancer-cli-rg --name lb-nsg
az network nsg rule create --resource-group load-balancer-cli-rg --nsg-name lb-nsg \
--name lb-nsg-rule --protocol '*' --direction inbound \
--source-address-prefix '*' --source-port-range '*' \
--destination-address-prefix '*' --destination-port-range 80 \
--access allow --priority 200
# the quickstart then creates, for management and for the backend:
# a Bastion host (public IP, AzureBastionSubnet 10.1.1.0/26, az network bastion create --sku Basic)
# two NICs (lb-VM1-nic, lb-VM2-nic) in backend-subnet, both with --network-security-group lb-nsg
# two VMs (lb-VM1 in zone 1, lb-VM2 in zone 2), az vm create --nics ... --no-wait
# a NAT gateway for outbound: public IP + az network nat gateway create --idle-timeout 10,
# attached with az network vnet subnet update --nat-gateway lb-nat-gateway
for vmnic in lb-VM1-nic lb-VM2-nic; do
az network nic ip-config address-pool add --address-pool lb-backend-pool \
--ip-config-name ipconfig1 --nic-name $vmnic \
--resource-group load-balancer-cli-rg --lb-name load-balancer
done
az network public-ip show --resource-group load-balancer-cli-rg \
--name lb-frontend-ip --query ipAddress --output tsv
az group delete --name load-balancer-cli-rg
What each step does:
- Resource group, virtual network, subnet. The subnet backend-subnet (10.1.0.0/24) holds the servers. (Everything in one group means one az group delete removes it all: the last command.)
- Public IP, --sku Standard --zone 1 2 3. A Standard public IP is required for a Standard load balancer (Basic and Standard SKUs cannot be mixed). The zone setting makes it zone-redundant: it survives the loss of a zone. --zone 1 would pin it to one.
- az network lb create. Makes the load balancer, with a frontend and an empty backend pool.
- The probe: TCP, port 80.
- The rule: frontend port 80 to backend port 80, with the probe attached. Look at the last three options: --disable-outbound-snat true (this rule must not also provide outbound translation: the NAT gateway does that job), --idle-timeout 15 (minutes) and --enable-tcp-reset true (see the idle section).
- NSG and rule: allow inbound port 80. The NICs of the servers must belong to an NSG, as Microsoft says for Standard.
- Bastion: a managed way to reach the servers for administration without public IPs. Microsoft's note: hourly pricing starts the moment Bastion is deployed, whether or not you use it. Delete it when you are done.
- Servers: two NICs and two VMs, in zones 1 and 2, each NIC in the NSG. The quickstart uses a Windows Server image and installs IIS with a script extension that writes the computer name into the default page.
- Add the NICs to the backend pool: the address-pool add loop. Only now do the servers receive traffic.
- NAT gateway for outbound access (explained below).
- Test: ask for the public IP address with az network public-ip show and browse to it.
A Linux version. The quickstart uses Windows. For two Ubuntu web servers you would replace the image and the IIS step with a cloud-init file passed as custom data (az vm create --custom-data), and the page would then answer with the server's name. Here is such a file, validated offline with cloud-init schema (the schema checks the shape, not whether the packages exist), next to a deliberately broken one that shows the three most common mistakes. The image name and the az vm create options for a Linux image were not run or checked here: list current images with az vm image list before you copy anything. (More on user data in First Boot on Every Cloud.)
$ cat web-init.yaml
#cloud-config
package_update: true
packages:
- nginx
write_files:
- path: /var/www/html/index.html
content: |
<h1>Hello from a backend server</h1>
runcmd:
- [ sh, -c, 'echo "<p>served by $(hostname)</p>" >> /var/www/html/index.html' ]
- [ systemctl, restart, nginx ]
$ cloud-init schema --config-file web-init.yaml
Valid schema web-init.yaml
$ cat bad-init.yaml
#cloud-config
package_update: yes please
packages: nginx
runcmd:
- systemctl restart nginx
write_files:
- path: /var/www/html/index.html
content: hello
permissions: 644
$ cloud-init schema --config-file bad-init.yaml
Error: Cloud config schema errors: package_update: 'yes please' is not of type 'boolean', packages: 'nginx' is not of type 'array', write_files.0.permissions: 644 is not of type 'string'
Error: Invalid schema: user-data
(six WARNING lines about unreadable files under /etc/cloud and an unavailable datasource are left out: this is an ordinary desktop, not a cloud VM.)
Testing it (expected, not run): browse to the public IP several times from different machines, or run curl in a loop from a few different places, and you should see the two server names appear, not strictly alternating, because the choice is a hash. A browser that reuses one connection (keep-alive) keeps seeing the same server, because the hash depends on the source port, which only changes with a new connection. To see failover, stop the web server on one machine and wait the probe time computed above.
Outbound traffic: the surprise
Servers behind a load balancer also need to call out: updates, APIs, package repositories. How their private addresses are translated to public ones is called SNAT, and Microsoft ranks the ways to do it:
- NAT Gateway on the subnet: "the best method". Dynamic ports, no exhaustion worries, and it takes precedence over every other method.
- A public IP on the VM: fine, but one IP per server.
- Load balancer outbound rules: explicit control, "OK" but "not at scale".
- The load balancer's frontend without outbound rules (implicit): fixed number of ports, "worst".
- Default outbound access: an address Azure hands out that you do not control and which can change. "Not recommended as it's insecure". From 31 March 2026, new virtual networks default to private subnets, which is Microsoft's way of retiring it.
(That is why the quickstart creates a NAT gateway and sets --disable-outbound-snat true on the rule.) A VM in a Standard load balancer's pool loses the default outbound access, so a server that could browse the internet before you added it to the pool can suddenly stop.
The reason this matters is SNAT port exhaustion. Each outbound connection to the same destination IP and port needs its own SNAT port. One public IP provides about 64,000 of them, and when several servers share a load balancer's frontend (without a NAT gateway), the ports are divided among the servers. With the default allocation Microsoft gives this table for one frontend IP: 1 to 50 servers get 1,024 ports each, 51 to 100 get 512, 101 to 200 get 256, 201 to 400 get 128, 401 to 800 get 64, 801 to 1,000 get 32; extra frontend IPs add more, up to 1,024. When a server runs out, new outbound connections to a destination fail until a port frees up: it shows as random timeouts that vanish and come back. Microsoft's cure list: a NAT gateway, connection reuse in the application, and, for Azure PaaS services, Private Link, which avoids SNAT completely. A model of the arithmetic:
>>> def default_ports(pool, frontends=1):
... table = [(50, 1024), (100, 512), (200, 256), (400, 128), (800, 64), (1000, 32)]
... for upper, ports in table:
... if pool <= upper:
... return min(ports * frontends, 1024)
... raise ValueError('the documented table ends at 1,000 instances')
...
>>> for pool in (2, 50, 51, 100, 101, 400, 1000):
... print(f'{pool:5} VMs, 1 frontend IP: {default_ports(pool):5} SNAT ports each')
...
2 VMs, 1 frontend IP: 1024 SNAT ports each
50 VMs, 1 frontend IP: 1024 SNAT ports each
51 VMs, 1 frontend IP: 512 SNAT ports each
100 VMs, 1 frontend IP: 512 SNAT ports each
101 VMs, 1 frontend IP: 256 SNAT ports each
400 VMs, 1 frontend IP: 128 SNAT ports each
1000 VMs, 1 frontend IP: 32 SNAT ports each
>>> print('100 VMs, 2 frontend IPs:', default_ports(100, 2))
100 VMs, 2 frontend IPs: 1024
>>> print('100 VMs, 3 frontend IPs:', default_ports(100, 3), '(capped)')
100 VMs, 3 frontend IPs: 1024 (capped)
>>> def manual(frontends, instances): return frontends * 64000 // instances
...
>>> print('outbound rule, 1 IP, 100 VMs:', manual(1, 100))
outbound rule, 1 IP, 100 VMs: 640
>>> print('outbound rule, 2 IPs, 100 VMs:', manual(2, 100))
outbound rule, 2 IPs, 100 VMs: 1280
>>> print('outbound rule, 2 IPs, 20 VMs:', manual(2, 20))
outbound rule, 2 IPs, 20 VMs: 6400
>>> # every port used by a load-balancing or inbound NAT rule takes a block of 8 SNAT ports (if the blocks differ)
>>> rules = [80, 443, 8080]
>>> print(64000 - 8 * len(rules), 'SNAT ports left on one frontend IP with rules for', rules)
63976 SNAT ports left on one frontend IP with rules for [80, 443, 8080]
(The code reproduces the table and Microsoft's own example: 100 servers on one frontend IP get 512 ports each, and a second IP makes it 1,024. For manual allocation in an outbound rule Microsoft's formula is frontends x 64K / servers; the page itself says 64,000 ports per IP are eligible, so I used 64,000.) Other facts from that page: each port used by a load-balancing or inbound NAT rule takes a block of eight SNAT ports (the model's last line assumes rules in different blocks), fragmented packets are dropped unless the outbound path is an instance-level public IP, and outbound rules do not support secondary IPv4 configurations on a NIC.
Idle timeout and TCP reset
A flow that carries no packets for a while is forgotten. The default idle timeout is 4 minutes, configurable from 4 to 100 minutes on load-balancing and inbound NAT rules (outbound rules: 4 to 120; the quickstart sets 15). By default, the load balancer silently drops the flow at the timeout: neither side is told, and the next packet vanishes, which looks like a hung connection. With TCP reset on idle enabled on the rule, it sends a reset to both ends, so the application learns at once that the connection is gone and can open a new one. Microsoft's guidance: enable TCP reset for predictable behaviour; use TCP keepalives (with an interval shorter than the timeout) when you need connections to survive longer than the maximum, or if resets upset the application; use application-level keepalives when a proxy sits in between. The idle timeout is not supported for UDP rules, and not at all for internal HA-ports rules reached through a user-defined route. If both the rule and the frontend IP set a timeout, the frontend IP's wins.
The limits that bite
From the components page:
- Only TCP and UDP. Ping (ICMP) does not work through the load balancer (an internal Standard one with HA ports is the exception). So "I cannot ping the load balancer's IP" is expected: test with a TCP connection.
- A backend server cannot reach its own load balancer's frontend if it is an internal load balancer: "Outbound flow from a backend VM to a frontend of an internal load balancer fails". A server that calls the internal address of the service it belongs to will not get through.
- A rule cannot span two virtual networks.
- IP fragments are not forwarded.
- A pool cannot contain a Private Endpoint.
- A subscription move of a Standard load balancer is not supported (a resource group move within a subscription is).
- Per availability set: one public and one internal NIC-based load balancer.
The neighbours: Application Gateway, Front Door, Traffic Manager
Microsoft's architecture guide sorts them by two questions: global or regional? and web (HTTP/HTTPS) or not?
| Service | Scope | Works on | Good for |
|---|---|---|---|
| Load Balancer | Regional (or global with the cross-region tier) | Layer 4: TCP and UDP | Any TCP/UDP service, very high speed and low delay, balancing VMs inside a network |
| Application Gateway | Regional | Layer 7 (HTTP/HTTPS), also TCP and TLS proxying | URL routing, TLS offload, web application firewall in front of your web servers |
| Front Door | Global | HTTP/HTTPS | Global web entry point, caching, acceleration, fast failover |
| Traffic Manager | Global | DNS | Steering clients to regions by DNS answer; failover is slower because of DNS caching and TTLs |
Two vocabulary words from the same page: a passthrough load balancer (this one) lets the client connect directly to the chosen server; a terminating one (Application Gateway, Front Door) accepts the client's connection and opens a separate one to the server. Microsoft stresses that real applications often use more than one, for example Front Door at the edge and an internal Load Balancer behind it. For anything that is a web application needing path routing or TLS handling, start from Application Gateway, not from this page.
What it costs
From the pricing page (no prices are quoted here, because the page I received showed placeholders instead of numbers: pick your region and currency on the page or in the pricing calculator). Standard Load Balancer is billed on three meters: an hourly charge per rule (the first five rules at one rate, additional rules at another), data processed (inbound and outbound, independent of rules), and inbound NAT rules, which are free. "There is no hourly charge for the Standard Load Balancer when no rules are configured." Gateway Load Balancer has its own meters. Cross-region (global tier) traffic to regional balancers adds no extra charge for the routing hop. And the things around it bill by the hour too: the public IP, the NAT gateway, and the Bastion from the quickstart. Clean up with the resource group delete.
When it does not work: a checklist
(From the documentation pages above; none of it was tested.)
- The portal or CLI shows the server as unhealthy. Is the web server really listening on the probe port? A TCP probe needs the port open; an HTTP probe needs a 200.
- Is the probe allowed in? Allow 168.63.129.16 (tag AzureLoadBalancer) in the NSG and the server's own firewall.
- Is the server running? A stopped VM is not probed.
- Traffic from the internet does not arrive. Standard is closed by default: is there an NSG on the NIC or subnet that allows the port? Is the rule's frontend port the one you are testing?
- Outbound calls time out after you added the server to the pool: the default outbound access is gone. Use a NAT gateway (or an outbound rule, or an instance public IP). Occasional failures on a busy server: SNAT exhaustion.
- "The connection was closed" after minutes of silence: the idle timeout (4 minutes by default). Enable TCP reset, raise the timeout, or use keepalives.
- Everything goes to one server. Check the distribution mode (Client IP affinity), and whether many users arrive through one proxy or SNAT address.
- Probes fail intermittently on a server with several network cards. Answer the probe on the same interface it arrived on.
- Calling the internal load balancer from one of its own servers fails: a documented limitation.
- You blocked the probe with an NSG to test failure and the result looks odd: Microsoft calls that an unsupported scenario that can take effect late. Stop the web server instead.
What this page did not cover
- Anything run on Azure. The commands are Microsoft's, untested here. The pricing numbers were not shown on the page I got.
- The portal steps (the quickstart exists for it; not read), PowerShell, Bicep/ARM and Terraform versions.
- Gateway Load Balancer, cross-region (global tier) load balancing, multiple frontends, IPv6 and dual stack, HA ports setup, admin state.
- Monitoring: Azure Monitor metrics, the Insights dashboards and health event logs exist (the overview lists them) but were not read in detail.
- Upgrading from Basic (Microsoft has a guidance page; not read), limits and quotas (the numbers are in the subscription limits page; not read).
- Application Gateway, Front Door and Traffic Manager beyond the one comparison.
- Whether the availability or latency of the service matches the documents: those are Microsoft's claims.
Sources
All from Microsoft, read on 9 October 2026 (the last-updated dates shown on the pages range from May to September 2026):
- What is Azure Load Balancer?
- Azure Load Balancer components
- Health probes
- Distribution modes
- SKUs
- Outbound connections (SNAT)
- TCP reset and idle timeout
- Quickstart: create a public load balancer with the CLI
- Load balancing options (Architecture Center)
- Load Balancer pricing
The cheat sheet
az network public-ip create --sku Standard --zone 1 2 3 the front door (Standard IP for a Standard LB) az network lb create --sku Standard --public-ip-address ... the load balancer, with a frontend and an empty pool az network lb probe create --protocol tcp --port 80 who is alive (http/https need --path) az network lb rule create --frontend-port 80 --backend-port 80 the traffic rule (needs a probe) az network nsg rule create ... --access allow --priority 200 Standard is CLOSED until an NSG allows it az network nic ip-config address-pool add ... put a server's NIC in the pool az network nat gateway create + subnet update --nat-gateway the recommended way for servers to call out az network public-ip show --query ipAddress --output tsv what to browse to az group delete --name GROUP removes everything (including the hourly-billed bits) Standard only (Basic retired 30 Sep 2025); layer 4 only: no URLs, no TLS, no ping Probe source 168.63.129.16 = tag AzureLoadBalancer; idle timeout 4 min default; default outbound access: avoid
Azure Load Balancer is the doorman who sends each guest to a different table, never reads the menu, and checks every five (or fifteen) seconds whether the kitchen is still alive.
← Azure Services: A Map for People Who Know Linux · ↑ All tutorials