Learn Computer Networking
From “what happens after you press Enter?” down to sequence numbers and longest-prefix match. Every lesson starts from the problem the protocol solves, asks “why does this exist?”, distinguishes IPv4 from IPv6 and models from implementations, and most carry a simulator you can step through and break.
Press Enter on a URL and follow the request: layers, encapsulation, and an educational packet inspector.
Why? — What actually happens after you press Enter?
Between pressing Enter on `https://engineer-atlas.dev` and seeing a page, a name becomes an address, an address becomes a path, a path carries a connection, the connection is secured, and only then does HTTP say a word — and each step has its own way of failing.
The same `GET /` seen at fifteen rungs — browser, socket API, kernel, transport, IP, link, NIC, switch, router, NAT, ISP, internet, server network, server kernel, server process — with the exact state that changes at each one: which headers are added, which addresses are rewritten, and which are never touched.
Networking is stacked because each layer solves one problem for the layer above without knowing what it carries; the four-layer TCP/IP model describes what actually runs, the seven-layer OSI model is a vocabulary — and modern protocols such as TLS, QUIC and ARP refuse to sit in one box.
Each layer wraps the one above in a header the layer below never reads: 100 bytes of HTTP become a 120-byte TCP segment, a 140-byte IP packet and a 154-byte Ethernet frame — and the 1500-byte MTU, the MSS derived from it, and path-MTU discovery decide how a large response is cut up so that nothing on the way has to fragment it.
A single captured frame carrying an HTTP request, decoded layer by layer — Ethernet (src MAC, dst MAC, EtherType), IP (src, dst, TTL, protocol), TCP (ports, seq, ack, flags), HTTP — with, for every field, who wrote it, who reads it, and who is allowed to change it.
Local delivery: frames, MAC addresses, switches, broadcast domains, ARP for IPv4 and neighbor discovery for IPv6; switches versus routers.
Why? — I know the IP address. How does the frame find the right cable?
Ethernet moves frames between interfaces that share a segment, addressed by MAC; a switch learns which port each MAC lives behind by watching source addresses, floods what it has not learned, and turns one shared cable into a set of private conversations — Wi-Fi is a different medium with the same addressing idea.
A MAC address is a 48-bit link-layer identifier meant to be unique on a segment: it names an interface for the switches and neighbours on that segment, is rewritten at every router, is trivially changeable, and is now randomised per network by phones and laptops — which is why "the MAC identifies the machine" is wrong in every direction that matters.
A host that knows the next hop’s IP still needs its MAC to build the frame: IPv4 asks with an ARP broadcast and caches the answer; IPv6 replaces ARP entirely with Neighbor Discovery over ICMPv6 multicast, which also carries router advertisements and lets hosts configure their own addresses.
A switch forwards frames within one network using MAC addresses and a learned table; a router forwards packets between networks using IP addresses and a routing table, decrementing TTL and rebuilding the frame at every hop — and the box under your desk is a router, a switch, an access point, a NAT and a DHCP server in one case.
Best-effort packet delivery, address structure, CIDR and subnetting, why IPv6 exists, NAT translation tables and what a port really is.
Why? — How does `192.168.1.42` know whether `10.0.0.7` is next door or across the world?
IP is the one protocol every device on the internet speaks: a source address, a destination address, a hop counter and a payload, forwarded hop by hop with no promise of delivery, order or uniqueness — a deliberately thin contract that leaves reliability to the ends and lets routers stay stateless.
`192.168.1.42` is 32 bits split by a mask into a network part that routers care about and a host part that only the last router does; the private ranges, loopback, link-local and broadcast addresses are carved out of the same space, and the space ran out — which is why NAT and IPv6 exist.
IPv6 gives every device a globally routable address and removes NAT as a necessity — but it also redesigns the header, replaces ARP and broadcast with ICMPv6 multicast, lets hosts configure themselves from router advertisements, forbids router fragmentation, and coexists with IPv4 through dual-stack and Happy Eyeballs rather than replacing it.
`10.0.0.0/24` is 256 addresses with 254 usable; move the mask one bit right and it becomes two `/25`s of 128 — subnetting is prefix arithmetic, and it exists to bound broadcast domains, draw security boundaries and let routers aggregate many networks into one route.
An IP address reaches a machine; a 16-bit port reaches one program on it — `203.0.113.10:443` names the HTTPS listener. The 4-tuple of both addresses and both ports identifies a connection, which is why one server port serves thousands of clients and why a client that opens too many connections to one destination runs out of ports.
A NAT rewrites the private source `10.0.0.4:54001` to the public `203.0.113.7:62014` on the way out, records the mapping in a table, and reverses it on the way back — which lets a household or a data centre share one address, and which is the reason inbound connections, peer-to-peer, long-lived idle sockets and high connection rates all need special handling.
Routing tables, longest-prefix match, next hops, and how the internet is stitched together from autonomous systems with BGP.
Why? — A router sees a destination IP and has three matching routes. Which wins, and why?
Every host and every router answers the same question for every packet — "given this destination address, which interface and which neighbour?" — by looking the address up in a routing table and taking the most specific match.
Given `10.0.0.0/8 → A`, `10.10.0.0/16 → B` and `0.0.0.0/0 → gateway`, the destination `10.10.42.7` goes to B because sixteen of its leading bits match that entry and only eight match the other — and this rule is what lets a router summarise the world into one entry and still carve out exceptions.
The internet is tens of thousands of independently run networks that tell each other which prefixes they can reach and by what path, and choose among those paths by commercial policy rather than by distance — which is why your packets take the route they take and why the return path is usually different.
From a name to an address: caches, recursive resolvers, root, TLD and authoritative servers, record types, TTLs — and a failure simulator.
Why? — Where does the IP for `engineer-atlas.dev` actually come from, and who is allowed to be wrong about it?
Humans want `engineer-atlas.dev`, routers want `203.0.113.10`, and no single file or server could hold the mapping for every name on earth — so DNS splits the namespace into a hierarchy of zones, delegates each to its owner, and lets a resolver near you walk that hierarchy and cache what it learns.
A name is resolved by falling through caches — browser, OS, recursive resolver — and, on a full miss, by the resolver walking root → TLD → authoritative, with every hop’s answer stored for its TTL; which cache you hit explains both the speed and why two people get different answers.
A zone is a set of typed records — A/AAAA for addresses, CNAME for aliases, MX and TXT for mail and verification, NS and SOA for delegation and zone metadata, SRV and PTR for service discovery and reverse lookup — each with rules that bite (CNAME at the apex, chain cost) and a TTL you choose deliberately, especially before a migration.
DNS fails in a small number of distinct ways — cache expiry, a dead resolver, a wrong record, an over-long TTL, a change in flight — and each produces a different signature (NXDOMAIN, SERVFAIL, timeout, a stale or split answer) that tells you which cache or server to look at.
Datagrams over IP with no connection, no ordering and no retransmission — and why DNS, real-time media and QUIC choose exactly that.
Why? — Why would anyone want a transport that can lose your data?
A reliable ordered byte stream over an unreliable network: the handshake, sequence numbers, acknowledgments, loss recovery, flow control, congestion control, head-of-line blocking and the connection lifecycle.
Why? — How do you build a reliable stream out of packets that can be lost, duplicated and reordered?
IP loses, duplicates, reorders and delays packets and says nothing about it; TCP turns that into a connection over which bytes arrive exactly once, in order, at a rate the receiver and the network can absorb — by numbering every byte, acknowledging what arrived, retransmitting what did not, and windowing what is in flight — and it hands the application a stream, not messages.
SYN, SYN-ACK, ACK: three segments in which each side proposes its initial sequence number and hears the other’s acknowledged, options are agreed, and the server moves the connection from a half-open queue to the accept queue — one full round trip before a single byte of data, which is the cost every short connection pays.
Every byte in a TCP stream has a number; a segment carries the number of its first byte, an ACK carries the number of the next byte the receiver wants, and out-of-order segments wait in a reassembly buffer until the hole before them is filled — which is why numbers are byte offsets, why cumulative ACKs cannot describe a gap, and why SACK exists.
When a segment is lost, TCP learns it either from three duplicate ACKs (fast, one round trip) or from a retransmission timer (slow, at least 200 ms on Linux and doubling), retransmits, and — because loss is also read as congestion — cuts its sending rate; the application sees only a stall, and a loss rate of 1% can cost most of a link’s throughput.
The receiver tells the sender, in every ACK, how many more bytes it has room for; the sender never has more than that in flight; when the application stops reading, the buffer fills, the window goes to zero and the sender stops — so a slow consumer throttles a fast producer all the way back through the network, which is backpressure by design.
No router tells a sender how much capacity is left, so the sender probes: it keeps a congestion window that grows exponentially, then linearly, and shrinks sharply when loss or an ECN mark says a queue is full — the sawtooth that shares links fairly, and the mechanism that CUBIC and BBR each implement with different signals.
Because TCP delivers bytes strictly in order, one lost segment holds back every byte behind it even when those bytes have already arrived — a property that HTTP/1.1 dodged with six parallel connections, that HTTP/2’s multiplexing made worse by putting every request on one stream, and that HTTP/3 addresses with QUIC’s independent streams, without eliminating ordering costs inside a stream or in the application.
A TCP connection is a state machine on both ends: established by the handshake, torn down by a FIN in each direction, aborted by RST — and the side that closes first sits in TIME_WAIT for a minute holding the port pair, while a side that never calls close() sits in CLOSE_WAIT forever; these states explain ECONNRESET, ephemeral-port exhaustion, descriptor leaks, and why many short connections are expensive.
TCP gives transport but not confidentiality or identity: the handshake, key agreement, certificates and the chain of trust.
Why? — How does my browser know it is talking to the real server, and not the coffee-shop Wi-Fi?
TCP delivers bytes reliably to an address; it says nothing about who is reading them, who is changing them, or whether the address is the server you meant — TLS is the layer that adds confidentiality, integrity and authenticated identity on top of a transport that has none.
Before the first encrypted byte, client and server negotiate a version and cipher suite, the server proves its identity with a certificate and a signature, and both derive the same session key from an ephemeral key exchange — one round trip in TLS 1.3, two in TLS 1.2, on top of TCP’s own.
A certificate binds a hostname to a public key with a signature from an issuer the client already trusts; the client walks leaf → intermediate → root, checks names, dates and signatures, and every classic TLS outage is one of those checks failing.
Requests, responses, headers and status codes; then the evolution from HTTP/1.1 through HTTP/2 multiplexing to HTTP/3 over QUIC; keep-alive and connection pooling.
Why? — Why did HTTP need three redesigns of its transport?
HTTP turns a byte stream into a request — method, path, headers, optional body — and a response — status code, headers, optional body; the method tells intermediaries whether a request is safe to retry and the status code tells the client what to do next.
A `fetch()` becomes an HTTP message inside TLS records inside TCP segments inside IP packets, crosses the network, is unwrapped in reverse on the server, handled, and returns the same way — and on a cold connection most of the time is round trips, not work.
HTTP/1.1 keeps the TCP connection open between requests and delimits bodies with `Content-Length` or chunks, but it can only carry one response at a time per connection — so browsers open several connections per origin, and text parsing costs bytes and ambiguity on every message.
HTTP/2 replaces text lines with binary frames tagged by stream id, so many requests and responses interleave on a single TCP connection with compressed headers — at the cost that one lost TCP segment now stalls every stream on that connection.
HTTP/3 runs over QUIC, a UDP-based transport that folds TLS 1.3 into its handshake, gives each stream independent loss recovery, and survives a change of IP address — reducing, not eliminating, head-of-line blocking, at the price of userspace CPU and UDP-hostile networks.
Three versions with identical semantics and three different transports: sequential text over TCP, multiplexed frames over one TCP connection, and multiplexed streams over QUIC — each is the right choice for a different link and a different deployment.
Reusing a connection skips the TCP and TLS handshakes and starts with a grown congestion window — but both ends, and every proxy between them, close idle connections on their own timers, and the race between a server closing and a client reusing produces the sporadic `ECONNRESET` every production system eventually meets.
Opening a connection costs handshakes, authentication and a cold congestion window, so clients keep a pool of open ones and hand them out per request — HTTP pools hold stateless connections any request can use, database pools hold connections that carry session state, and both fail the same way when the pool runs dry.
Getting data from the server without asking: polling, server-sent events and WebSockets compared on directionality, infrastructure, reconnection and scale.
Why? — The server has news. How does it tell a browser that only ever asks?
A WebSocket starts as an HTTP request that asks to switch protocols, gets `101 Switching Protocols`, and from then on the same TCP connection carries framed messages in both directions for as long as both sides want — which makes every proxy, load balancer and idle timer between them part of the design.
Four ways to get data from a server that has news to a client that only asks: repeated requests, requests the server holds open, a one-way HTTP stream with built-in reconnection, and a bidirectional socket — each trades infrastructure simplicity against latency and directionality.
Forward vs reverse proxies, L4 vs L7 balancing, health checks, and the networking view of a CDN: DNS, edge locations, caches and geographic distance.
Why? — When the client connects to "the server", what is it actually connected to?
A proxy is a process that terminates one connection and opens another; whether it acts for the client (forward) or for the server fleet (reverse) decides who knows about it, what it may rewrite, and which headers you can trust.
A load balancer either forwards TCP connections it never reads (L4: cheap, protocol-blind) or terminates HTTP and routes requests it understands (L7: smarter, costlier) — and in both cases its health checks, draining and idle timeouts decide what 502 and 504 mean.
A CDN moves the TCP/TLS endpoint and a cache to within a few milliseconds of the client, because the one cost no engineering removes is the speed of light in fibre; what it cannot cache still pays the trip to the origin.
Rules that allow or deny traffic by address, port and protocol; stateful inspection; encrypted tunnels.
Why? — A ping fails but the service works. What is a firewall actually blocking?
A firewall is an ordered rule list evaluated against each packet’s addresses, ports and protocol — stateful ones remember which connections you started so replies get in — and a silent drop looks completely different on the wire from nothing listening.
A VPN wraps whole IP packets inside encrypted packets, adds a virtual interface and a route that sends chosen destinations through it — and every byte of wrapper is a byte of MTU your packets no longer have.
Network namespaces, virtual interfaces, bridges, and just enough Kubernetes to understand pod IPs, services, ingress and network policies.
Why? — Two containers on one host both bind port 80. How is that not a conflict?
A Linux network namespace is a private copy of the whole network stack — interfaces, addresses, routes, firewall rules, ports — so two containers can each bind :80 and never meet; a veth pair is the virtual cable that connects a namespace back to the host.
A container’s eth0 is one end of a veth pair on a host bridge; published ports are DNAT rules in the host kernel, container DNS is a tiny resolver the runtime injects, and `localhost` inside the container is the container — not the host.
Kubernetes gives every pod a routable IP with no NAT between pods, turns a Service into a virtual IP that the node kernel rewrites to a live pod, exposes L7 routing as an ingress proxy, resolves names through cluster DNS, and lets a network policy be a namespaced firewall — all implemented by a pluggable CNI.
RTT, bandwidth, loss, retransmission, connection and TLS setup, queueing — and why a request is slow while the server CPU sits idle.
Why? — The server is idle and the request still takes 800 ms. Where did the time go?
Round-trip time spans five orders of magnitude from loopback to cross-continent, every request costs at least one of them and a cold HTTPS request three or four, so a chatty pattern that is invisible in one data centre is a multi-second disaster across an ocean.
Bandwidth is how many bytes a second the pipe carries; latency is how long the first byte takes; transfer time is latency plus size over bandwidth, and a single TCP connection cannot use a fat, long pipe unless its window covers the bandwidth-delay product.
Throughput is work per unit time in whichever unit you are limited by — requests, packets or bytes — and it is tied to latency by Little’s law and bounded by loss and RTT through TCP’s congestion control, so measuring one without the other tells you very little.
A request’s wall-clock time is a sequence of segments — DNS, TCP, TLS, request, queueing in every buffer along the way, server work, response, retransmissions — and most of them can be large while the server’s CPU is idle; label each one and the slow request explains itself.
"Why can’t I connect?" as a layered procedure, the tools and which layer each answers, DNS/TCP/TLS/HTTP debugging — and the capstone: what happens when you visit `https://example.com`.
Why? — Connection timed out. Which of the nine layers failed?
Every connection failure lives in exactly one layer, and the fastest way to find it is to bisect the ladder — DNS, route, host, port, handshake, TLS, HTTP, application — reading the failure signature at each step instead of guessing.
ping, traceroute, dig, curl, ss, tcpdump, Wireshark, nc and openssl are not a list to memorise; each one asks a question at one layer and is blind to the others, so choosing the tool is choosing the layer you are testing.
ping sends an ICMP echo request and reports whether a reply came back and how long it took; that answers "does this host respond to ICMP right now" and nothing else — a failed ping does not mean a service is down and a successful ping does not mean it is up.
traceroute sends probes with TTL 1, 2, 3… and collects the ICMP Time Exceeded replies each router returns when it discards them, revealing the forward path one hop at a time — and the same mechanism is why the output is full of honest-looking lies.
A name lookup can be answered by half a dozen different caches and resolvers before it reaches anyone authoritative, so the first question in every DNS problem is "which of them answered?" — and `dig` against a chosen server, `+trace` and the TTL in the answer will tell you.
"Connection times out" has exactly three wire signatures — SYN/SYN-ACK/ACK, SYN then RST, or SYN into silence — and one `ss` on the server plus one `tcpdump` on each end converts a vague timeout into a named cause: nothing listening, wrong bind address, dropped by a firewall, host down, backlog full or ephemeral ports exhausted.
"Certificate invalid" is the browser’s summary of six unrelated faults — wrong name, expired, missing intermediate, wrong clock, wrong certificate for the SNI, protocol or cipher mismatch — and `openssl s_client` plus `curl -v` name which one in a single line each.
The three gateway errors usually describe three different upstream situations — 502 the backend answered wrongly or closed, 503 no backend was available, 504 the backend did not answer in time — and the debugging procedure is to find which hop generated the status, correlate with that hop’s upstream logs and health state, and check that the timeout ladder is ordered.
The canonical networking interview question is a test of whether you can tell the story at the right altitude — nine steps in a minute, twelve layers on request, and at every layer the mechanism, the state that changes, and the failure you would name.
Follow one packet hop by hop and inspect its state, then inject failures — loss, latency, DNS outage, expired certificate, blocked port — and predict the behaviour.
Why? — What changes in the packet at each hop, and what happens when a hop fails?
Pick a source, a destination, a protocol and a port, and follow a single packet from `send()` through the socket, transport, IP and link layers, out of the NIC, through a NAT router and every hop after it, watching which headers exist, which fields change and which stay fixed — and what a router does in the microseconds it holds the packet.
Eight buttons — drop a packet, add 200 ms, add 5% loss, kill DNS, kill a router, expire the certificate, block the port, reset from the server — each produce a distinct symptom at the application, a distinct chain of events through the layers, and a distinct tool that confirms it; the skill is predicting all three before pressing the button.
Follow `send()` through the socket API, the kernel, the transport stack and the NIC to a server that wakes up in `recv()`; build a tiny server from blocking to event-driven; buffers, backpressure, zero-copy and a combined failure simulator.
Why? — What actually happens between writing `send()` and another machine’s process waking up?
Between `send()` returning in one process and `recv()` returning in another there are two kernels, two NICs, three copies, a congestion gate, a routing decision and at least one context switch — and every one of them is a place where bytes wait.
Six versions of the same server, each one born from the specific failure of the previous one: a single request, a blocking loop, a thread per client, a pool, non-blocking sockets, and finally an event loop.
accept → read → process → write → next: the simplest correct server, and the clearest demonstration that a blocking call parks the whole program on one client’s behaviour.
Give every client its own thread and let the scheduler interleave them: the code stays sequential and the OS supplies the concurrency — until the number of threads becomes the workload.
A fixed set of workers pulling connections from a bounded queue: thread cost becomes a constant, overload becomes a queue length you can see, and the slow client returns as "one slow request occupies a worker".
One thread, many non-blocking sockets, and a kernel API that says which ones are ready: the server sleeps until something happens and then does exactly the work that is possible — as long as nothing in it ever blocks.
The limits are concrete and countable: threads, descriptors, kernel socket memory, wake-up cost, ephemeral ports, middlebox state — and each has a mechanism that moved it, which is why the number went from 10K to 10M without the laws of physics changing.
Application buffer → socket send buffer → device queue → wire → NIC ring → socket receive buffer → application: a chain of bounded queues in which every full buffer pushes back on the one above, sized by bandwidth × delay and dangerous when oversized.
A fast sender and a slow reader: the receive buffer fills, the window closes, the send buffer fills, and the sender’s `write()` blocks, returns EAGAIN, returns false, or awaits — depending only on which I/O model it chose. Buffer in user space instead and it fails by running out of memory.
Serving a file the naive way copies it four times and crosses the user/kernel boundary four times; `sendfile`, `splice`, scatter-gather DMA and, at the extreme, kernel bypass remove the copies the CPU does not need to make — until TLS puts one back.
High-throughput systems are built by letting the page cache be the shared buffer between disk, process and NIC, and by batching every crossing of the user/kernel boundary: Kafka’s log, a database’s buffer pool, `writev`, and io_uring.
Application → kernel → network → remote: exhaust descriptors, fill a buffer, block a thread, drop packets, add latency, kill the process, restart the server — and follow each failure across the layers to the symptom the user sees and the tool that proves it.
A user in Warsaw opens your application hosted in another region and the page takes three seconds: enumerate every layer where the time could be, assign each to the domain that explains it, put a typical cost and a measurement next to each, and bisect.