Module 1 · Network Foundations · Lesson 1 of 18
How a Request Crosses the Network: OSI, TCP/IP, IP Addresses and Ports for .NET Developers
Learning outcome
You can explain what happens between await http.GetAsync(url) and the first byte of the response, layer by layer. You can map the OSI and TCP/IP models to things you meet in .NET (HttpClient, SslStream, Socket, IP addresses), explain what an IP address, a port and the connection 4-tuple identify, and estimate the cost of a cold HTTPS request in round trips. These are the foundations every later lesson in this course builds on, and the first ten minutes of most backend networking interviews.
Intuition
Networking is a stack of layers, each solving one problem and trusting the layer below it. As an application developer you live at the top, but almost every production mystery (timeouts, socket exhaustion, stale DNS, slow first requests) comes from behaviour two or three layers down. If you can say which layer a symptom belongs to, you already know which tools to reach for.
Each layer wraps the one above, like envelopes inside envelopes. Your JSON goes inside an HTTP message, which TLS encrypts, which TCP cuts into numbered segments, which travel inside IP packets, which travel inside Ethernet or Wi-Fi frames. The receiver unwraps them in reverse.
flowchart TB
A["Application: HTTP, gRPC, DNS / HttpClient, Kestrel"] --> S["Security: TLS / SslStream, certificates"]
S --> T["Transport: TCP, UDP, QUIC / Socket, ports"]
T --> N["Network: IP / addresses, routing"]
N --> L["Link: Ethernet, Wi-Fi / MAC addresses, one hop"]Deep dive
OSI vs TCP/IP: which model to use in an interview
The OSI model has seven layers: physical, data link, network, transport, session, presentation and application. The TCP/IP model (the one the internet actually uses) has four or five: link, internet (IP), transport (TCP/UDP), and application. Interviewers still use OSI numbers as shorthand, so learn the mapping:
| OSI layer | Name | What lives there | .NET touch point |
|---|---|---|---|
| 7 | Application | HTTP, gRPC, DNS, SMTP | HttpClient, Kestrel, Dns |
| 6 | Presentation | encoding, compression, TLS (by convention) | SslStream, System.Text.Json |
| 5 | Session | connection lifetime, resumption | TLS sessions, HTTP/2 streams |
| 4 | Transport | TCP, UDP (QUIC runs on UDP) | Socket, TcpClient, UdpClient |
| 3 | Network | IP, ICMP, routing | IPAddress, ping |
| 2 | Data link | Ethernet, Wi-Fi, ARP, MAC addresses | the NIC driver |
| 1 | Physical | cables, radio | none |
Two phrases you will hear: an L4 load balancer forwards TCP or UDP connections by IP and port without reading HTTP (for example Azure Load Balancer or AWS NLB). An L7 load balancer or reverse proxy terminates HTTP and can route by path, header or cookie (for example YARP, NGINX, Azure Application Gateway, AWS ALB). Lesson 11 builds one.
TLS does not fit OSI neatly. For HTTP/1.1 and HTTP/2 over TLS, it sits between TCP and HTTP. HTTP/3 instead integrates TLS 1.3 with QUIC over UDP.
IP addresses
An IP address identifies a network interface, not a program. IPv4 addresses are 32 bits (20.51.88.12), IPv6 addresses are 128 bits (2001:db8::1). Some ranges matter in backend work:
127.0.0.0/8and::1are loopback: traffic never leaves the machine.10.0.0.0/8,172.16.0.0/12and192.168.0.0/16are private (RFC 1918). Cloud virtual networks use them, and they are not routable on the internet. To reach the internet they go through NAT.0.0.0.0(or[::]) in a listen address means "all interfaces". A container that listens on127.0.0.1cannot be reached from outside the container. This is a classic Docker bug: Kestrel must listen onhttp://0.0.0.0:8080(orhttp://+:8080) inside the container.
CIDR notation (10.1.0.0/16) means "the first 16 bits are the network". Subnets, network security groups and Kubernetes network policies are all written this way.
Ports and the 4-tuple
A port is a 16-bit number (0 to 65535) that says which program on a machine a segment is for. Servers listen on well-known ports (80 HTTP, 443 HTTPS, 53 DNS, 1433 SQL Server, 5432 PostgreSQL, 6379 Redis). When your code makes an outgoing connection, the OS picks a free ephemeral port as the source. On Linux the default range is 32768 to 60999. On Windows it is 49152 to 65535.
A TCP connection is identified by its 4-tuple: (source IP, source port, destination IP, destination port). Two connections from the same client to the same server differ only in source port. So:
- A server can hold thousands of connections on port 443, because each client side of the tuple is different.
- A client calling the same destination can open only about as many simultaneous connections as it has free ephemeral ports. This asymmetry is why clients run out of ports and servers do not (Lesson 8).
The journey of one cold HTTPS request
When HttpClient has no pooled connection to the host, one GET pays for:
| Step | Round trips | Notes |
|---|---|---|
| DNS lookup (on cache miss) | 1 to the resolver, maybe more upstream | Lesson 3 |
| TCP three-way handshake | 1 RTT | Lesson 2 |
| TLS 1.3 handshake | 1 RTT (TLS 1.2: 2) | Lesson 5 |
| Request and first response byte | 1 RTT + server time | |
| Body | bandwidth-bound | TCP slow start limits a new connection |
RTT (round-trip time) inside one cloud region is about 1 ms. Between India and Western Europe it is roughly 120 to 150 ms. So a cold cross-region HTTPS call takes about 3 RTTs (roughly 360–450 ms) to the first response byte even with negligible server processing, plus any DNS lookup time. On a warm pooled connection, steps 1 to 3 disappear and the request costs 1 RTT plus server time. That one fact justifies connection pooling, keep-alive and most of what SocketsHttpHandler does.
Latency vs bandwidth
Bandwidth is how much data per second a link carries. Latency is how long one trip takes. Light in fibre travels about 200 km per millisecond, so no amount of money removes the floor on a long path. API-to-API traffic is usually latency-bound: payloads are small and round trips dominate. The levers, in order: fewer round trips (reuse connections, batch, avoid N+1 API calls), shorter trips (co-locate services), and parallel trips (Task.WhenAll for independent calls).
Code example: one listener, many connections
The program opens a listening socket on loopback and connects three clients. It prints each connection's 4-tuple and the ephemeral port range of the machine.
using System.Net;
using System.Net.Sockets;
// One listening socket on a fixed local port; every accepted connection is a new socket.
using var listener = new TcpListener(IPAddress.Loopback, 0); // port 0 = let the OS pick a free port
listener.Start();
var serverEndPoint = (IPEndPoint)listener.LocalEndpoint;
Console.WriteLine($"Server listening on {serverEndPoint}");
var clients = new List<TcpClient>();
for (var i = 1; i <= 3; i++)
{
var client = new TcpClient(AddressFamily.InterNetwork);
await client.ConnectAsync(serverEndPoint); // TCP handshake happens here
using var accepted = await listener.AcceptTcpClientAsync(); // server-side socket for this client
clients.Add(client);
var local = (IPEndPoint)client.Client.LocalEndPoint!;
var remote = (IPEndPoint)client.Client.RemoteEndPoint!;
var serverSide = (IPEndPoint)accepted.Client.LocalEndPoint!;
Console.WriteLine($"Connection {i}: 4-tuple = ({local.Address}, {Mask(local.Port)}) -> ({remote.Address}, {remote.Port}); " +
$"server side uses local port {serverSide.Port}");
}
var ports = clients.Select(c => ((IPEndPoint)c.Client.LocalEndPoint!).Port).ToList();
Console.WriteLine($"Distinct client (ephemeral) ports: {ports.Distinct().Count()} of {ports.Count}");
Console.WriteLine($"All connections share the server port: {serverEndPoint.Port}");
if (File.Exists("/proc/sys/net/ipv4/ip_local_port_range"))
{
var range = File.ReadAllText("/proc/sys/net/ipv4/ip_local_port_range").Split((char[])['\t', ' ', '\n'], StringSplitOptions.RemoveEmptyEntries);
int low = int.Parse(range[0]), high = int.Parse(range[1]);
Console.WriteLine($"Linux ephemeral port range: {low}-{high} ({high - low + 1} ports per destination IP:port)");
}
foreach (var c in clients) c.Dispose();
// Ephemeral ports change on every run, so the sample prints them masked.
static string Mask(int port) => port >= 1024 ? "<ephemeral>" : port.ToString();Output (the server port is chosen by the OS and differs per run):
Server listening on 127.0.0.1:34917 Connection 1: 4-tuple = (127.0.0.1, <ephemeral>) -> (127.0.0.1, 34917); server side uses local port 34917 Connection 2: 4-tuple = (127.0.0.1, <ephemeral>) -> (127.0.0.1, 34917); server side uses local port 34917 Connection 3: 4-tuple = (127.0.0.1, <ephemeral>) -> (127.0.0.1, 34917); server side uses local port 34917 Distinct client (ephemeral) ports: 3 of 3 All connections share the server port: 34917 Linux ephemeral port range: 32768-60999 (28232 ports per destination IP:port)
What to notice:
- All three connections share the server's port. The server tells them apart by the client's ephemeral port.
- The ephemeral range is finite: about 28,000 ports on Linux for each destination IP and port. Lesson 8 shows how fast a careless client uses them up.
- Port
0inTcpListener(IPAddress.Loopback, 0)asks the OS for any free port. Tests should use this instead of hard-coded ports.
Failure modes
- Listening on loopback inside a container. Kestrel on
localhostinside Docker is unreachable from the host. Listen on0.0.0.0or setASPNETCORE_HTTP_PORTS. - Assuming the network is reliable. IP drops, duplicates and reorders packets. Everything reliable is built on top, and it costs round trips.
- Blaming bandwidth for latency. A bigger pipe does not fix a chatty protocol with 40 sequential calls.
- Ignoring the first request. Cold DNS, TCP and TLS make the first call slow. Warm up critical paths at startup, or accept and measure it.
- Mixing up L4 and L7. An L4 balancer cannot route by URL path or add
X-Forwarded-For; an L7 proxy can.
Interview angle
"What happens when you type a URL into a browser?" or "What happens when your service calls another service?" Answer it as a timeline: "DNS resolves the name, using caches first. The client opens a TCP connection with a three-way handshake, one round trip. TLS 1.3 adds one more round trip: the server proves its identity with a certificate chain and both sides derive session keys. Then the HTTP request goes out, the server processes it and the response streams back. On a pooled connection the first three steps are skipped, which is why we reuse connections." Mention the layers as you go and offer to go deeper on any step. That shows structure, not memorised trivia.
Practice questions
- What does the 4-tuple identify, and why do clients run out of ports before servers do?
Answer: One TCP connection: source IP, source port, destination IP, destination port. A server reuses its one listening port for every connection; each client connection to the same destination needs its own ephemeral source port, and there are only tens of thousands of them.
- Kestrel in a container listens on
http://localhost:8080, andcurlfrom the host gets "connection refused". Why?
Answer: Inside the container, localhost is the container's own loopback interface. Traffic forwarded from the host arrives on the container's network interface. Listen on 0.0.0.0:8080.
- Roughly how many round trips does a cold HTTPS GET cost before the first response byte, and how many on a warm connection?
Answer: About 3 or more (DNS on a miss, TCP 1, TLS 1.3 1, request 1) versus 1 on a pooled connection.
- What is the difference between an L4 and an L7 load balancer?
Answer: L4 forwards TCP/UDP flows by IP and port and does not read HTTP. L7 terminates HTTP and can route by host, path or header, rewrite headers and retry.
- A dashboard calls 30 microservice endpoints one after another and is slow, though each call is fast on its own. What lever helps most?
Answer: Fewer and parallel round trips: batch endpoints or call independent ones concurrently with Task.WhenAll, over pooled connections. More bandwidth would not help.
Revision checklist
- Name the OSI layers and map them to TCP/IP and .NET types.
- IPv4 vs IPv6, loopback, private ranges,
0.0.0.0in listen addresses. - Ports: well-known vs ephemeral; the 4-tuple.
- Cold request cost: DNS + TCP + TLS + request; warm request: 1 RTT.
- Latency vs bandwidth; the three levers.
Related on TechInterviewNotes
Analogy
Addresses, rooms and envelopes
The IP address is a building's street address; the port is the room number. When you call out, reception gives you a temporary room as your return address, and there are only so many of those rooms. Your letter goes into an envelope (HTTP), sealed in a tamper-proof pouch (TLS), handed to a courier who numbers the pages (TCP), who uses the postal network (IP).
Quick reference
Layers and numbers
- L7 HTTP/gRPC/DNS, TLS between L4 and L7, L4 TCP/UDP, L3 IP.
- 4-tuple = src IP, src port, dst IP, dst port.
- Linux ephemeral ports 32768-60999; Windows 49152-65535.
- Cold HTTPS: about 3 RTTs before the first byte; warm: 1 RTT.
- In containers listen on 0.0.0.0, not localhost.
- Latency-bound: fewer, shorter, parallel round trips.