The worker 10.1.4.21 in network A calls https://api.acme.example/orders?page=2 and waits for
200 OK. It asks its resolver 10.1.0.53 for the address over UDP, opens a connection to 10.3.2.5
port 443 in network B and sends a request. What does each step do, and what does it cost?
Intent: a resolver finds a name’s address by asking the root, a top-level domain (TLD) server and
the zone’s server, ns1.acme.example at 203.0.113.53 on the internet, and keeps it for its TTL. A TCP handshake (SYN, SYN-ACK, ACK; Part 3) and TLS
(Part 8) open a connection between two sockets, each an address and a port. HTTP sends its request,
a GET, over it, and each new connection costs round trips that a kept one and a cache save.
A name becomes an address#
Who turns api.acme.example into 10.3.2.5? The worker 10.1.4.21 in network A does not know,
and at first neither does its resolver. DNS (the Domain Name System) is a database spread over many servers on
the internet, plus the protocol to ask them.
In the figures of this part a wire is a path. The routers of Parts 4 and 5 sit on it, but they are not drawn.
The names are made up. RFC 2606 keeps .example for documentation, so no real TLD server answers
for it, but a real name takes the same walk.
Four kinds of server#
A name is read from the right. The root knows who serves example, the TLD (top-level domain)
server knows who serves acme.example, and that zone’s own server knows api.acme.example.
| Server | Holds | Answers with | Who runs it |
|---|---|---|---|
| root | where the servers of each top-level domain are | a referral: “ask the .example servers” | 12 operators run the 13 root server names |
| top-level domain (TLD) | for each zone under it, its NS records and, when the name server’s name is inside the zone, its address (glue) | a referral to ns1.acme.example, at 203.0.113.53 | the registry of that domain |
| authoritative | the records of one zone, here acme.example | the answer: api.acme.example is 10.3.2.5 | the zone’s owner, or a DNS provider it pays |
| recursive resolver | nothing at the start; then a cache of every answer | the final answer, to the worker | your network, or your cloud provider: here 10.1.0.53 |
Each root name answers from many sites at once, by anycast (Part 5), ICANN’s root server FAQ says. As of 2026-10-15, root-servers.org counted 2,047 instances in all.
The resolver is a recursive resolver: it does the whole walk for its clients (RFC 9499 sec. 6). The
worker learned its address, 10.1.0.53, from DHCP with its lease (Part 4).
Recursive and iterative#
The worker asks once, with the RD (recursion desired) bit set: “find the answer for me”. RFC 9499 calls this a recursive query. The resolver then asks each server in turn without that request, and follows each referral: this is iterative resolution.
With nothing cached, one name costs eight messages: four questions and four answers. The figure
shows the old habit of sending the full name to every server. A resolver that follows RFC 9156 sends
each server only the part of the name it needs: the root is asked about example alone.
An everyday picture: you ask one clerk, and the clerk goes from desk to desk for you. Each desk only says which desk to try next. The picture breaks at memory: the clerk keeps every answer for a set time, and nobody tells the clerk when it changes (next section).
What a record holds#
Every answer is made of resource records. The A record that ns1.acme.example sends has five fields
(RFC 1035 sec. 3.2.1).
| Field | In this answer | Means |
|---|---|---|
| name | api.acme.example | the name asked about |
| type | A | the kind of data: here an IPv4 address |
| class | IN | the internet; nearly every record has it |
| TTL | 300 | the seconds a resolver may keep the record |
| data | 10.3.2.5 | the value; its form depends on the type |
| Type | Holds |
|---|---|
| A | an IPv4 address |
| AAAA | an IPv6 address |
| CNAME | another name that this name stands for: an alias |
| NS | the name of a server for a zone, such as ns1.acme.example for acme.example |
| MX | a mail server for the domain, with a preference number |
| TXT | free text |
A name with a CNAME holds no other record, not even an A or an MX (DNSSEC signatures aside). RFC 1034 sec. 3.6.2 and RFC 2181 sec. 10.1 say so.
The .example TLD holds two records for acme.example. One is the NS record that names ns1.acme.example;
the other is that server’s A record, 203.0.113.53. The second is glue: without it, the resolver
would need the server’s address in order to ask that same server for it.
UDP first, TCP too#
The worker’s question to 10.1.0.53 goes in one UDP datagram to port 53, and the answer comes back
in one. There is no connection to set up, so the worker’s question costs one round trip to the resolver.
With an empty cache the resolver adds its own walk (above).
RFC 1035 prefers UDP for queries for its lower overhead.
Plain DNS over UDP carries at most 512 bytes. A larger answer comes back cut short, with the TC (truncated) bit set, and the client asks again over TCP port 53. EDNS(0) lets a client announce a larger UDP size, so fewer answers are cut (RFC 6891).
So TCP is not a spare: every general-purpose DNS implementation must support both UDP and TCP (RFC 7766 sec. 5). Plain DNS is not encrypted. Newer transports hide the question and the answer; the table lists two.
| Transport | Port | Used when |
|---|---|---|
| DNS over UDP | 53 | first, for a plain lookup |
| DNS over TCP | 53 | the UDP answer came back truncated |
| DNS over TLS (DoT) | TCP 853 | the client wants the query encrypted (RFC 7858) |
| DNS over HTTPS (DoH) | 443 by default, inside HTTPS | the same, carried as ordinary web requests (RFC 8484) |
DNSSEC is a different thing. It signs a zone’s answers so a resolver can detect a forged one, but it does not hide them (RFC 4033 sec. 1).
In a VPC#
In a VPC you do not have to run a resolver yourself. Each provider runs one for every VM, and DHCP points the VM at it.
| Provider | The resolver a VM uses | Private names |
|---|---|---|
| Google Cloud | the VM’s metadata server, 169.254.169.254, given with the DHCP lease | a Cloud DNS private zone answers only queries from the VPC networks you authorize |
| AWS | the Route 53 Resolver, at 169.254.169.253 and at the VPC’s primary range plus two (10.0.0.2 in 10.0.0.0/16) | a private hosted zone answers inside the VPCs you name for it |
| Alibaba Cloud | 100.100.2.136 and 100.100.2.138, set by DHCP on instances from official images | a Private DNS zone answers inside the VPCs of its effective scope |
The provider runs the resolver and its cache. You still write the records of each private zone, pick which networks may see the zone, and set each record’s TTL.
The answer is kept for its TTL#
A minute later the worker 10.1.4.21, in network A, calls the api again. The resolver does not walk the tree a second time: it
answers from its cache until the record’s TTL runs out.
The zone’s owner sets the TTL of each record, here 300 seconds. The resolver counts it down: asked at 60 s, it answers with TTL 240, the time left.
The TTL is a maximum, not a promise: a resolver may drop a record sooner (RFC 2181 sec. 8). RFC 8767 recommends capping a TTL at 604,800 seconds, 7 days. It also lets a resolver keep serving an expired record while the zone’s servers cannot be reached.
At 310 s the A record has expired, but the NS records for .example and acme.example have not:
here their TTLs are longer. So the resolver skips the root and the TLD and asks ns1.acme.example,
203.0.113.53 on the internet, directly, four messages instead of eight.
Negative answers are kept too#
A name that does not exist gets the answer NXDOMAIN, and that answer is cached as well. Its lifetime comes from the zone’s SOA (start of authority) record, which the server sends with the answer. It is the lower of the SOA’s own TTL and its minimum field (RFC 2308 sec. 5).
The minimum field is the zone’s own value for how long a “no” may be kept.
So a record created right after a failed lookup can stay invisible for that long.
On the worker#
Programs do not speak DNS themselves. They call getaddrinfo(), which returns the addresses for a
name and a service (getaddrinfo(3)). The C library’s resolver code, a stub resolver that asks but
never walks, reads the resolver’s address from /etc/resolv.conf (resolv.conf(5)):
nameserver 10.1.0.53dig api.acme.example asks the same resolver and prints its answer. Three lines like these matter.
| Line | Reads | Means |
|---|---|---|
| header | status: NOERROR | the name exists and the query worked; NXDOMAIN means the name does not exist |
| flags | qr rd ra | a response (qr), recursion desired (rd), recursion available (ra); tc would mean the answer was cut |
| answer section | api.acme.example. 300 IN A 10.3.2.5 | name, TTL left, class, type, data |
A socket: address plus port#
The answer is an address, 10.3.2.5, the api’s in network B. Many programs can run on the api’s host, so the worker also
needs a port: the number of one program’s door. A socket is a program’s end of a connection, named
by an address and a port.
Ports#
A port is a 16-bit number. RFC 6335 sec. 6 splits the range into three parts.
| Range | Name | Used for |
|---|---|---|
| 0-1023 | system ports | well-known services, such as HTTPS on 443 |
| 1024-49151 | user ports | registered services, such as PostgreSQL on 5432 |
| 49152-65535 | dynamic ports | the client’s side of a connection; never assigned to a service |
Linux picks a client’s port from 32768 to 60999 by default (ip_local_port_range). The worker’s
connection to the api comes from 51514, inside that range.
| Port | Service, in the IANA registry | In this series |
|---|---|---|
| 53, UDP and TCP | DNS | this part; Part 6 |
| 80, TCP | HTTP without TLS | this part |
| 443, TCP and UDP | HTTPS | this part; Part 8 |
| 5432, TCP | PostgreSQL | db-b1, Parts 3 and 8 |
One listening socket, many connections#
The api’s server program asks the kernel to listen on port 443 (listen(2)). Each time a client
connects, accept() creates a new socket for that connection and leaves the listening socket as it
was (accept(2)). A SYN to a port where nothing listens, such as 8080 on the api, gets a reset (RST,
Part 3) back, unless a firewall drops it first (RFC 9293 sec. 3.10.7.1).
A TCP connection is named by five values: the protocol, two addresses and two ports. The worker’s two
connections share four of them and differ only in the source port, 51514 and 51515. So one port
443 holds many connections at once, from one client or from many.
ss -tan on the api prints the listening socket and one row per connection (ss(8); columns trimmed).
The 0.0.0.0 means any local IPv4 address.
State Local Address:Port Peer Address:Port
LISTEN 0.0.0.0:443 0.0.0.0:*
ESTAB 10.3.2.5:443 10.1.4.21:51514
ESTAB 10.3.2.5:443 10.1.4.21:51515SYN, SYN-ACK and ACK in the figure are the TCP handshake, which Part 3 explains. The ESTAB row
appears after the worker’s ACK, and only then does accept() hand the program its socket.
A byte stream, not messages#
TCP hands the receiver every byte, in order, but keeps no edges between messages. RFC 9293 sec. 3.7 says TCP “guarantees no correlation” between the edges of its segments and the program’s own writes and reads.
So HTTP/1.1 marks where each message ends, with Content-Length or with chunks (RFC 9112 sec. 6).
UDP keeps each datagram whole, but promises neither delivery nor protection from duplicates (RFC
768).
| TCP | UDP | QUIC | |
|---|---|---|---|
| connection | yes, after a handshake (Part 3) | none | yes, its handshake carries TLS 1.3 |
| delivery | every byte, in order | a datagram may be lost or arrive twice | every byte of each stream, in order |
| message edges | none: one byte stream | each datagram is one message | none inside a stream |
| used here by | HTTP/1.1, HTTP/2, large DNS answers | DNS | HTTP/3, over UDP |
The request and the response#
On the open connection, after TLS (Part 8), the worker 10.1.4.21 writes its request as lines of
text, and reads the response of the api 10.3.2.5 the same way. This is HTTP/1.1. HTTP/2 and HTTP/3 carry the same fields in binary
frames (next section).
The URL https://api.acme.example/orders?page=2 has four parts: the scheme https, the host
api.acme.example, the path /orders and the query page=2 (RFC 3986). Only the path and the query
travel in the request line.
curl -v --http1.1 https://api.acme.example/orders?page=2 prints what it sends on lines starting with
> and what it receives on lines starting with < (curl(1)). Without --http1.1 it speaks HTTP/2
and prints the names in lowercase. The lines that matter:
Request line or header (>) | Holds |
|---|---|
GET /orders?page=2 HTTP/1.1 | the method, the target and the version |
Host: api.acme.example | the name the client asked for, since one server address can carry many names |
Accept: application/json | the format the client wants back |
| (an empty line) | the end of the headers; this GET has no body |
Host is mandatory in every HTTP/1.1 request (RFC 9112 sec. 3.2).
Status line or header (<) | Holds |
|---|---|
HTTP/1.1 200 OK | the version, the status code and its reason |
Content-Type: application/json | the format of the body |
Content-Length: 1840 | the body’s size in bytes: where this response ends |
ETag: "v7" | a tag for this version of the page (Caches, below) |
Cache-Control: max-age=60 | how long a cache may reuse the response: 60 s |
| (an empty line) | the end of the headers |
| the body | 1,840 bytes of JSON |
Methods and status codes#
A method is safe when it asks for no change. It is idempotent when sending it twice has the same effect as once. So a client may retry it after a lost response (RFC 9110 sec. 9.2).
| Method | Does | Safe | Idempotent |
|---|---|---|---|
| GET | reads the target | yes | yes |
| HEAD | the same as GET, headers only | yes | yes |
| POST | sends data for the server to process, often to create something | no | no |
| PUT | replaces what is at the target | no | yes |
| DELETE | removes it | no | yes |
| Code | Means (RFC 9110 sec. 15; 429: RFC 6585 sec. 4) |
|---|---|
| 200 OK | it worked |
| 301, 308 | moved for good, to the address in Location; after a 301 a client may turn a POST into a GET, after a 308 it keeps the method |
| 304 Not Modified | the client’s cached copy is still good; the response has no body |
| 400 Bad Request | the server could not read the request |
| 401 Unauthorized, 403 Forbidden | 401: no valid credentials; 403: understood, and refused |
| 404 Not Found | nothing at that target |
| 429 Too Many Requests | the client sent too many requests in a given time; the response may say when to retry (Retry-After) |
| 500 Internal Server Error | the server failed |
| 502 Bad Gateway | a proxy got a bad answer from the server behind it |
| 503 Service Unavailable | overloaded or in maintenance, for now |
| 504 Gateway Timeout | a proxy got no answer in time from the server behind it |
State rides in headers#
HTTP is stateless: “each request message’s semantics can be understood in isolation” (RFC 9110 sec. 3.3). A
server that wants to recognise a client later sends Set-Cookie: name=value. The client returns
Cookie: name=value on its later requests (RFC 6265 sec. 4.1.1).
An API client sends its credentials in the Authorization header of each request instead (RFC 9110
sec. 11.6.2).
Round trips before the first byte#
The api 10.3.2.5 is 50 ms from the worker 10.1.4.21, there and back: the RTT of Part 1. Every exchange that must wait for an answer
costs one RTT before the next step can start. How many does one call pay?
| Step | Messages | Cold call | On a kept connection |
|---|---|---|---|
| TCP handshake (Part 3) | SYN, SYN-ACK, ACK | 1 RTT | none |
| TLS 1.3 handshake (Part 8) | ClientHello; ServerHello to Finished; Finished | 1 RTT | none |
| request and response | GET, 200 OK | 1 RTT | 1 RTT |
| total at 50 ms | 150 ms | 50 ms |
The worker’s ACK and its TLS Finished leave with the next message, so they add no wait. A DNS lookup comes on top when the name is not in a cache.
These are network round trips only: the api’s own time and the time to send the bytes are not counted. Resuming an earlier TLS session with 0-RTT data (Part 8) would save one round trip. HTTP/3 sets up the connection and TLS together, so its cold call costs two, 100 ms.
Keep the connection#
HTTP/1.1 keeps a connection open after each response by default (RFC 9112 sec. 9.3). The next request on it skips both handshakes: 50 ms instead of 150 ms.
A server closes a connection that stays idle past its own timeout (RFC 9112 sec. 9.5). A client library keeps a pool of open connections and reuses them.
On one HTTP/1.1 connection, the next request waits for the previous response. A client may send several without waiting, called pipelining, but browsers leave it off by default. So browsers commonly open six connections to one host instead (MDN).
HTTP/2: many requests on one connection#
HTTP/2 (RFC 9113) keeps the methods, status codes and header meanings, but sends them as binary frames. Each request gets its own stream, and the frames of different streams interleave on one connection. A client numbers its streams with odd numbers: here 1, 3 and 5 for pages 1, 2 and 3.
One problem stays. TCP delivers bytes in order, so one lost packet holds up every stream behind it until it is resent.
This is head-of-line blocking: the first in the queue blocks everyone behind it. RFC 9113 sec. 1 says HTTP/2 does not solve it.
HTTP/3: the same over QUIC#
HTTP/3 (RFC 9114) carries the same requests over QUIC, a transport that runs over UDP (RFC 9000). QUIC carries the TLS 1.3 handshake inside its own packets (RFC 9001), so the connection and its encryption are set up together. A lost packet stalls only the streams whose data it carried (RFC 9000 sec. 13).
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| runs on | TCP | TCP | QUIC, over UDP |
| requests at once on one connection | one | many, as streams | many, as streams |
| a lost packet holds up | the connection | every stream of the connection | only its own streams |
| share of requests to Cloudflare in 2025 | 29 %, with HTTP/1.0 | 50 % | 21 % |
These shares count requests to one large network, not the whole web.
Caches: answers kept closer#
The api 10.3.2.5 answered the worker 10.1.4.21 with Cache-Control: max-age=60. For the next 60 seconds a client with a cache may
reuse it without asking. After that, it asks whether the page changed, without downloading it again.
Fresh or stale#
A stored response is fresh while its age is under its freshness lifetime. The lifetime comes from
max-age in Cache-Control (RFC 9111 sec. 4.2.1).
Without max-age or an Expires header, a cache may guess. RFC 9111
sec. 4.2.2 names 10 % of the time since Last-Modified as a typical setting.
Asking whether it changed#
At 75 s the copy is stale. The worker sends the same GET with If-None-Match: "v7": send the page
only if its tag is no longer "v7". The api answers 304 Not Modified with no body, so the copy is
fresh again for 60 s and 1,840 bytes stay at the api.
When the list changes, the next check, at 140 s, gets 200 OK, the new body and ETag: "v8". The worker
replaces its copy.
An older form asks by date, with If-Modified-Since: Fri, 16 Oct 2026 03:00:00 GMT; HTTP dates are
written in GMT (RFC 9110 sec. 5.6.7). When a request carries both, the server must ignore the date
and use the tag (sec. 13.1.3).
Where caches sit#
| Where | Kind (RFC 9111 sec. 1) | Serves | What it can read |
|---|---|---|---|
| inside the client: a browser, or an HTTP library set up with a cache | private cache, “a component of a user agent” | one user | its own user’s responses |
| a reverse proxy or a CDN in front of the api | shared cache | every client | every response: it acts as the origin, so the client’s TLS ends there (RFC 9110 sec. 3.7) |
| a forward proxy the client is set to use | shared cache | the clients behind it | plain http, or HTTPS that it decrypts; inside a CONNECT tunnel, where the proxy only passes encrypted bytes on, nothing it can cache (RFC 9110 sec. 3.8, 9.3.6) |
A CDN (content delivery network) is many such caches in many places. DNS can send a client to one of them, by answering with the address of a site near the asking resolver. One anycast address can do the same: the routes pick the site (Part 5).
When it breaks#
| Case | Description | Check with | Why | Fix |
|---|---|---|---|---|
| a host moved, clients did not | the api has a new address, but some clients still call the old one | dig api.acme.example on a client’s resolver: the old address and the TTL left | resolvers keep the old A record until its TTL runs out | lower the TTL at least one old TTL before the move, then change the record |
| a new name still fails | a record was just created, and lookups still say it does not exist | dig: status NXDOMAIN, and the SOA in the authority section with its TTL | the resolver cached the negative answer for the lower of the SOA’s TTL and its minimum field | wait it out, or clear the resolver’s cache; create records before anything looks them up |
| large DNS answers fail | small lookups work; a name with many records or signatures times out | dig +ignore shows the tc flag on the UDP answer; dig +tcp then times out | the answer is too big for UDP, and a firewall drops TCP port 53 | allow TCP 53 between clients and resolvers, and from resolvers to the internet |
| a pooled connection dies after idling | the first request after a quiet spell fails; a retry on a new connection works | the time since the connection’s last use, against the NAT’s idle timeout | a NAT on the path dropped the idle mapping: AWS NAT gateway after 350 s, answering the next packet with a reset (RST, Part 3); Google Cloud NAT after 1,200 s by default | close pooled connections sooner than the NAT’s timeout, or send TCP keepalives (small probes on an idle connection) more often than it |
| a cache serves an old answer | the api changed, and clients still get the old page | curl -v --http1.1: the < Age and < Cache-Control lines | max-age is too long, or a CDN in front still holds a fresh copy | shorten max-age, or let caches revalidate with an ETag |
Remember#
- A recursive resolver walks root, TLD and the zone’s server for you, and keeps each answer for its TTL.
- DNS asks over UDP port 53 first, and must also work over TCP when the answer is too big.
- A connection is two sockets, five values in all: one listening port holds many connections, told apart by the client’s port.
- A cold HTTPS call pays three round trips, a kept connection one: reuse connections, and close idle ones before a NAT drops them.
- A cache reuses a response until
max-ageruns out, then asks with the ETag, and304means nothing to download.
Sources#
Checked 2026-10-16.
- RFC 1034, Domain Names: Concepts and Facilities: CNAME holds no other data (sec. 3.6.2)
- RFC 3596, DNS Extensions to Support IP Version 6: the AAAA record
- RFC 1035, Domain Names: Implementation and Specification: the record format (sec. 3.2.1), the header (sec. 4.1.1), UDP and TCP on port 53 and the 512-byte limit (sec. 4.2)
- RFC 2181, Clarifications to the DNS Specification: the TTL is a maximum (sec. 8), CNAME alone at a name (sec. 10.1)
- RFC 2308, Negative Caching of DNS Queries
- RFC 2606, Reserved Top Level DNS Names
- RFC 6891, Extension Mechanisms for DNS (EDNS(0))
- RFC 7766, DNS Transport over TCP, RFC 9210, DNS Transport over TCP: Operational Requirements
- RFC 7858, DNS over TLS, RFC 8484, DNS Queries over HTTPS
- RFC 4033, DNS Security Introduction and Requirements
- RFC 8767, Serving Stale Data to Improve DNS Resiliency: the 7-day cap (sec. 4)
- RFC 9156, DNS Query Name Minimisation to Improve Privacy
- RFC 9499, DNS Terminology: recursive resolver, recursive query, iterative resolution, glue
- Root Server Technical Operations Association: 13 names, 12 operators, the instance count on 2026-10-15; ICANN, RSSAC FAQ: every root server identity is served by anycast
- RFC 6335, Port Number Procedures: the three ranges (sec. 6); IANA, Service Name and Port Number Registry
- RFC 768, User Datagram Protocol, RFC 9293, Transmission Control Protocol: sockets and connections (sec. 4), no message edges (sec. 3.7)
- RFC 3986, Uniform Resource Identifier: Generic Syntax
- RFC 9110, HTTP Semantics: stateless (sec. 3.3), intermediaries and caches (sec. 3.7-3.8), dates (sec. 5.6.7), methods (sec. 9), conditional requests (sec. 13.1), status codes (sec. 15)
- RFC 9111, HTTP Caching: private and shared caches (sec. 1), freshness (sec. 4.2)
- RFC 9112, HTTP/1.1: Host (sec. 3.2), message length (sec. 6), persistence and timeouts (sec. 9)
- RFC 9113, HTTP/2: head-of-line blocking (sec. 1), odd client streams (sec. 5.1.1)
- RFC 9114, HTTP/3, RFC 9000, QUIC, RFC 9001, Using TLS to Secure QUIC
- RFC 6265, HTTP State Management Mechanism
- RFC 6585, Additional HTTP Status Codes: 429 (sec. 4)
- MDN, Connection management in HTTP/1.x: pipelining off, six connections
- Cloudflare Radar 2025 Year in Review: HTTP version shares
- curl(1), Debian:
-vand the>and<lines; dig(1), resolv.conf(5), getaddrinfo(3), listen(2), accept(2), ss(8) - Linux kernel, IP sysctl:
ip_local_port_range - Google Cloud, Internal DNS, DNS zones overview, Tune NAT configuration
- AWS, Understanding Amazon DNS, Troubleshoot NAT gateways
- Alibaba Cloud, DHCP options sets and DNS hostnames, Private DNS for ECS instances