Frontend Labs
⋯

25. Network & Systems Mathematics

Level: Expert You'll use this for: subnet design, capacity planning, latency budgets, and understanding TCP behavior under load.

Subnetting / CIDR math

An IPv4 address is 32 bits. A /24 CIDR block means the first 24 bits are the fixed network portion, leaving 32 - 24 = 8 bits for host addresses: 2^8 = 256 addresses (254 usable, since the first is the network address and the last is the broadcast address).

def subnet_info(cidr_bits):
    total_addresses = 2 ** (32 - cidr_bits)
    usable_hosts = total_addresses - 2   # minus network + broadcast
    return total_addresses, usable_hosts

print(subnet_info(24))   # (256, 254)
print(subnet_info(16))   # (65536, 65534)

# How many /24s fit inside a /16?
def subnets_within(outer_cidr, inner_cidr):
    return 2 ** (inner_cidr - outer_cidr)

print(subnets_within(16, 24))   # 256 — a /16 contains 256 /24 networks

Subnet masking itself is literal bitwise AND (Ch. 3): an IP is "in" a subnet if ip & mask == network_address & mask.

import ipaddress
net = ipaddress.ip_network("10.0.0.0/24")
print(net.num_addresses, list(net.hosts())[:3])
ip = ipaddress.ip_address("10.0.0.55")
print(ip in net)   # True

Checking 10.0.0.55 against 10.0.0.0/24 bit by bit — convert both to binary, apply the mask (Ch. 1, 3), and compare:

IP:    10.0.0.55  =  00001010.00000000.00000000.00110111
Mask:  /24         =  11111111.11111111.11111111.00000000   <- 24 ones, then 8 zeros

IP AND Mask:          00001010.00000000.00000000.00000000  =  10.0.0.0
Network address:                                               10.0.0.0

They match  ->  10.0.0.55 IS inside the 10.0.0.0/24 network.

The AND mask zeroes out the last 8 bits (the host portion) on both sides — if what's left over matches the network address, the IP belongs to that subnet. The broadcast address is the same network bits with every host bit forced to 1 instead of 0: 10.0.0.0 with the last 8 bits set gives 10.0.0.255 — the top of the range, and the address the code's net.num_addresses (256) and usable-host count (254) are built from.

Bandwidth-delay product — why "just add more bandwidth" isn't the whole story

BDP = bandwidth × round-trip-time (RTT)

This tells you how many bytes can be "in flight" on the link at once — and therefore the minimum TCP window size needed to fully utilize the link. A 1 Gbps link with 100ms RTT has a BDP of 1,000,000,000 × 0.1 / 8 = 12.5 MB. If your TCP window is smaller than this (the classic default 64KB window is 200× too small here), you cannot use the full bandwidth no matter how fast the link is — this is the actual mathematical reason "long fat networks" (high bandwidth, high latency, e.g., transcontinental links) need TCP window scaling enabled.

def bandwidth_delay_product_bytes(bandwidth_bps, rtt_seconds):
    return (bandwidth_bps * rtt_seconds) / 8

bdp = bandwidth_delay_product_bytes(1_000_000_000, 0.1)
print(f"{bdp/1_000_000:.1f} MB needed in flight to saturate the link")

Little's Law and queueing theory (M/M/1)

Little's Law, astonishingly general (applies to any stable queueing system): L = λ · W (average number of items in the system = arrival rate × average time each item spends in the system). This applies to network packet queues, web server request queues, and literally any waiting line.

The M/M/1 queue (Poisson arrivals, exponential service time, 1 server) gives a closed-form for average wait time as a function of utilization ρ = λ/μ (arrival rate ÷ service rate):

Average time in system: W = 1 / (μ - λ)     (only valid when λ < μ)
def mm1_avg_wait(arrival_rate, service_rate):
    if arrival_rate >= service_rate:
        return float('inf')   # unstable queue: grows without bound
    return 1 / (service_rate - arrival_rate)

for util in [0.5, 0.7, 0.9, 0.95, 0.99]:
    mu = 100                       # server handles 100 req/sec
    lam = util * mu
    print(f"utilization={util:.0%}  avg wait={mm1_avg_wait(lam, mu)*1000:.1f}ms")

This is the single most important, most underused piece of math in capacity planning: wait time doesn't grow linearly with utilization — it grows hyperbolically, exploding as utilization approaches 100%. Going from 90% to 95% utilization doesn't add 5% more latency, it can double it. This is the rigorous justification for the common SRE rule of thumb "never run production at >70-80% sustained utilization."

TCP congestion control — AIMD (Additive Increase, Multiplicative Decrease)

TCP's congestion window cwnd grows additively (+1 MSS per RTT) when things are going well, and shrinks multiplicatively (halved) on packet loss:

def aimd_simulation(rounds, loss_at_round):
    cwnd = 1
    history = []
    for r in range(rounds):
        if r == loss_at_round:
            cwnd = cwnd / 2            # multiplicative decrease
        else:
            cwnd += 1                  # additive increase
        history.append(cwnd)
    return history

print(aimd_simulation(20, loss_at_round=10))

This produces the classic loss-based TCP "sawtooth" throughput graph. The asymmetry (slow linear growth, fast halving) is deliberately conservative — a direct, real-world application of control theory (Ch. 29) tuned to avoid congestion collapse across the shared, uncoordinated internet.

Deriving the M/M/1 formulas (instead of trusting them)

Symbol Meaning
λ arrival rate (Poisson arrivals, Ch. 14)
μ service rate (exponential service times)
ρ = λ/μ utilization, the fraction of time the server is busy. Must be < 1
πₙ long-run probability that n requests are in the system

The number of requests in the system is a Markov chain (Ch. 11): it goes up by 1 at rate λ and down by 1 at rate μ. In steady state, the flow across each boundary between n and n+1 must balance:

λ · πₙ = μ · πₙ₊₁     ->    πₙ₊₁ = ρ · πₙ     ->    πₙ = ρⁿ · π₀

The probabilities sum to 1, and Σ ρⁿ = 1/(1 - ρ) is a geometric series (Ch. 6), so π₀ = 1 - ρ:

πₙ = (1 - ρ) · ρⁿ                                (geometric distribution)
L  = Σ n · πₙ = ρ / (1 - ρ)                      (average number in system)
W  = L / λ = 1 / (μ - λ)                         (Little's Law, below)

Example: at ρ = 0.9 the average queue holds 0.9/0.1 = 9 requests.

Buffer overflow probability falls out for free: P(n ≥ k) = ρᵏ. A router port at 90% utilization exceeds 20 queued packets 0.9²⁰ ≈ 12% of the time and 50 packets 0.9⁵⁰ ≈ 0.5% of the time. Read it backwards to size a buffer for a target drop rate: k = ln(target) / ln(ρ).

Why Little's Law holds for any system

L = λ · W holds whatever the arrival or service distributions. The proof is a counting argument. Plot N(t), the number of requests in the system over a long window of length T, and compute the area under that curve two ways:

area = ∫₀ᵀ N(t) dt = L · T                    (average height × width)
area = Σ over requests of (time each one spent) = (λ·T) · W   (count × average stay)

L · T = λ · T · W     ->     L = λ · W

Each request contributes a horizontal strip as tall as one request and as long as its stay, so both sides measure the same area (an integral, Ch. 12). That's why Little's Law applies to a CPU run queue, a Kafka consumer group, a connection pool or a coffee shop.

Example: a service handles 2,000 req/s with an average latency of 50 ms, so L = 2000 · 0.05 = 100 requests are in flight on average. You need at least 100 concurrent connections or worker threads, plus headroom for the variance shown above.

Multiple servers: Erlang C, and why pooling wins

For c identical servers sharing one queue (M/M/c), with offered load a = λ/μ (in Erlangs), the probability that an arriving request has to wait is the Erlang C formula:

            (aᶜ / c!) · c/(c - a)
P_wait = ──────────────────────────────────────          W_q = P_wait / (c·μ - λ)
         Σₖ₌₀^(c-1) aᵏ/k!  +  (aᶜ / c!) · c/(c - a)
Symbol Meaning
c number of servers (workers, threads, agents)
a = λ/μ offered load in Erlangs: how many servers would be busy on average
P_wait the chance a request finds every server busy
W_q average time spent waiting before service starts

Example: sizing a worker pool. λ = 90 jobs/s and each worker completes μ = 10 jobs/s, so a = 9 Erlangs. Nine workers is the bare minimum, but:

Workers c Utilization P(wait) Avg queue wait
10 90% 67% 66.9 ms
11 82% 43% 21.5 ms
12 75% 27% 8.9 ms
14 64% 9% 1.8 ms

Going from 10 to 12 workers (+20% cost) cuts queueing delay 7.5×. That's the same hyperbolic curve as M/M/1, now with a precise number for the decision.

Pooling. Compare two separate M/M/1 servers, each getting 45 req/s with μ = 50, against one shared queue feeding both (M/M/2, 90 req/s):

separate queues:   W = 1/(50 - 45)                       = 200 ms
one pooled queue:  W = W_q + 1/μ  (Erlang C, c = 2)      ≈ 105 ms

The same hardware and the same load give half the latency, just by sharing the queue. A request never waits behind a busy server while the other one is idle. That's why one shared work queue beats per-worker queues, why a single supermarket line feeding many tills is faster than one line per till, and why work stealing (Go's scheduler, Tokio, Java's ForkJoinPool) exists. It recovers most of the pooling benefit while keeping per-worker queues for cache locality.

Variability is the enemy: Kingman's formula

Real traffic isn't Poisson and real service times aren't exponential. For a general single-server queue (G/G/1), Kingman's approximation gives the waiting time:

W_q ≈ ( ρ / (1 - ρ) ) · ( (c_a² + c_s²) / 2 ) · τ
       utilization       variability          service time
Symbol Meaning
c_a coefficient of variation (std ÷ mean, Ch. 9) of inter-arrival times. Poisson gives 1
c_s coefficient of variation of service times. Exponential gives 1, constant gives 0
τ mean service time

At ρ = 0.8 and τ = 10 ms:

Arrivals / service c_a², c_s² Wait
Poisson / exponential (M/M/1) 1, 1 40 ms
Poisson / constant service time 1, 0 20 ms
Bursty arrivals, heavy-tailed service 4, 4 160 ms

The three factors each say something different. Utilization explodes near

  1. Variability multiplies the wait linearly. Service time scales it. So cutting tail variance (timeouts, request size limits, splitting the slow endpoints onto their own pool) can matter as much as adding capacity. It's the reason "head-of-line blocking" by one huge request hurts everyone behind it.

Why AIMD converges to fairness

Two flows share a link of capacity 16, starting very unfairly at (x₁, x₂) = (10, 2). Each round, both add 1 if total ≤ 16, or both halve if the link overflows:

(10, 2) → (11, 3) → (12, 4) → (13, 5)          diff 8   (increase keeps the difference)
  cut   → (6.5, 2.5)                            diff 4   (halving halves the difference)
  ...   → (10.5, 6.5) → cut → (5.25, 3.25)      diff 2
  ...   → (9.25, 7.25) → cut → (4.6, 3.6)       diff 1

Additive increase preserves the gap between the flows. Multiplicative decrease halves it. After k congestion events the unfairness has shrunk by 2ᵏ, so the flows converge to an equal share whatever their starting point. Chiu and Jain (1989) proved that among linear controls, AIMD is the one that converges to both efficiency and fairness. AIAD keeps the unfairness forever, and MIMD keeps the ratio forever. That paper is why TCP behaves the way it does.

How big should a router buffer be?

The classic rule was "buffer = one bandwidth-delay product", so a single TCP flow's sawtooth never empties the queue. For a 10 Gbit/s link with 250 ms RTT that's 10¹⁰ · 0.25 / 8 = 312.5 MB of fast memory, which is expensive. Appenzeller, Keslassy and McKeown (2004) showed that with n desynchronized flows the sawtooths average out (the CLT again, Ch. 14: the total window's fluctuations grow like √n while the mean grows like n), so you need only:

buffer ≈ BDP / √n

With 10,000 flows, √n = 100 and the buffer drops to 3.1 MB. That's the difference between off-chip DRAM and on-chip SRAM. Oversized buffers cause bufferbloat: full queues that add seconds of latency without adding throughput. CoDel and FQ-CoDel (Linux's default qdisc on many distributions) attack it by managing the time packets spend queued rather than the queue length, which is Little's Law applied as a control signal.

Real-life engineering ties

  • Network: subnetting/CIDR is daily-driver math for any network engineer; BDP explains "why is my transcontinental transfer slow despite huge bandwidth"; queueing theory is the rigorous basis for capacity planning and SLA design.
  • SWE/SRE: Little's Law applies directly to any request queue, worker pool, or connection pool sizing decision.
  • Security: queueing math explains and helps mitigate SYN flood / slow-loris style DoS: an attacker's goal is to push ρ → 1 (or past it) deliberately, and understanding the hyperbolic wait-time curve explains why even modest additional load near saturation can cause disproportionate service degradation.

More real-life examples:

  • Google's BBR congestion-control algorithm (deployed widely since 2016, including at YouTube) replaced classic loss-based AIMD/CUBIC with a model that directly estimates the bandwidth-delay product from this chapter, rather than waiting for packet loss as its congestion signal.
  • CDN capacity planning (Netflix's Open Connect, Akamai, Cloudflare) applies Little's Law and queueing theory at enormous scale to decide how many edge servers and how much cache capacity a region needs to keep p99 latency low during peak load.
  • Cloud auto-scaling policies (AWS Target Tracking, Kubernetes HPA) are configured with a target utilization — commonly 60-70%, not 100% — a direct, practiced application of the M/M/1 hyperbolic wait-time curve.
  • Home routers and ISPs implementing Smart Queue Management (e.g., cake, fq_codel) apply Active Queue Management theory to reduce "bufferbloat," trading a little throughput for a large drop in latency.

Common mistake

Sizing infrastructure off average utilization instead of the queueing math above — average utilization looks "fine" (e.g., 60%) while p99 latency is already terrible, because traffic isn't smooth; it's Poisson (Ch. 14), so instantaneous utilization spikes well above the average regularly.


Generated from real-life-maths-guide.md · 8 Oct 2026 16:36