↓ Skip to main content

Names, ports and requests: DNS and HTTP before the first byte

Networking foundations - This article is part of a series.
Part 2: This Article

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.

Three phases. Name: the worker asks its resolver 10.1.0.53 over UDP port 53 for api.acme.example; the wires to root, the .example TLD and ns1.acme.example light in turn; the answer is 10.3.2.5, TTL 300. Connect: from port 51514 to port 443 of the api, SYN, SYN-ACK, ACK, then TLS. Request: GET /orders?page=2, then 200 OK with 1,840 bytes. A panel keeps what the worker has after each phase.
A name, a connection, a request: three exchanges before the first byte of the answer.

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.

Five phases. The worker sends one recursive query, RD set, over UDP port 53 to the resolver, which follows each referral. The root answers ask the .example servers; the .example TLD answers ask ns1.acme.example at 203.0.113.53 (glue); ns1.acme.example answers 10.3.2.5, TTL 300. Each answer stays beside its server, a counter reaches 8 of 8 messages, and a cache panel fills with two NS rows, the glue row and the A row.
The resolver asks three servers in turn: each one knows only the next step down the name.

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.

ServerHoldsAnswers withWho runs it
rootwhere the servers of each top-level domain area 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.53the registry of that domain
authoritativethe records of one zone, here acme.examplethe answer: api.acme.example is 10.3.2.5the zone’s owner, or a DNS provider it pays
recursive resolvernothing at the start; then a cache of every answerthe final answer, to the workeryour 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).

FieldIn this answerMeans
nameapi.acme.examplethe name asked about
typeAthe kind of data: here an IPv4 address
classINthe internet; nearly every record has it
TTL300the seconds a resolver may keep the record
data10.3.2.5the value; its form depends on the type
TypeHolds
Aan IPv4 address
AAAAan IPv6 address
CNAMEanother name that this name stands for: an alias
NSthe name of a server for a zone, such as ns1.acme.example for acme.example
MXa mail server for the domain, with a preference number
TXTfree 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.

TransportPortUsed when
DNS over UDP53first, for a plain lookup
DNS over TCP53the UDP answer came back truncated
DNS over TLS (DoT)TCP 853the client wants the query encrypted (RFC 7858)
DNS over HTTPS (DoH)443 by default, inside HTTPSthe 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.

ProviderThe resolver a VM usesPrivate names
Google Cloudthe VM’s metadata server, 169.254.169.254, given with the DHCP leasea Cloud DNS private zone answers only queries from the VPC networks you authorize
AWSthe 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 Cloud100.100.2.136 and 100.100.2.138, set by DHCP on instances from official imagesa 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.

Two phases. Cached, at 60 s: the resolver answers 10.3.2.5, TTL 240, from its cache: 2 messages, not 8. Expired, at 310 s: the A row has expired but the NS rows have longer left, so root and the .example TLD, drawn grey, are not asked; the resolver asks ns1.acme.example alone and gets TTL 300: 4 messages, not 8. The cache panel keeps both columns.
A cached answer costs no walk: after the TTL runs out, only the last server is asked again.

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.53

dig api.acme.example asks the same resolver and prints its answer. Three lines like these matter.

LineReadsMeans
headerstatus: NOERRORthe name exists and the query worked; NXDOMAIN means the name does not exist
flagsqr rd raa response (qr), recursion desired (rd), recursion available (ra); tc would mean the answer was cut
answer sectionapi.acme.example. 300 IN A 10.3.2.5name, 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.

Three phases between the worker and the api. Inside the api the program calls listen(443) and accept() per connection; an ss panel starts with LISTEN 0.0.0.0:443. Handshakes from ports 51514 and 51515, picked by the OS, each add an ESTAB row. With no firewall in the way, a SYN to port 8080 gets RST back: no listener, no row. A strip compares TCP and UDP: connection, delivery, message edges.
One listening socket, one new socket per connection: only the client’s port tells them apart.

Ports
#

A port is a 16-bit number. RFC 6335 sec. 6 splits the range into three parts.

RangeNameUsed for
0-1023system portswell-known services, such as HTTPS on 443
1024-49151user portsregistered services, such as PostgreSQL on 5432
49152-65535dynamic portsthe 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.

PortService, in the IANA registryIn this series
53, UDP and TCPDNSthis part; Part 6
80, TCPHTTP without TLSthis part
443, TCP and UDPHTTPSthis part; Part 8
5432, TCPPostgreSQLdb-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:51515

SYN, 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).

TCPUDPQUIC
connectionyes, after a handshake (Part 3)noneyes, its handshake carries TLS 1.3
deliveryevery byte, in ordera datagram may be lost or arrive twiceevery byte of each stream, in order
message edgesnone: one byte streameach datagram is one messagenone inside a stream
used here byHTTP/1.1, HTTP/2, large DNS answersDNSHTTP/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).

Two phases between the worker and the api on the open connection. Request: a panel fills with GET /orders?page=2 HTTP/1.1, Host: api.acme.example, Accept: application/json and an empty line. Response: a second panel fills with HTTP/1.1 200 OK, Content-Type, Content-Length: 1840, ETag, Cache-Control: max-age=60, an empty line and 1,840 bytes of JSON. Under them, four status codes: 200, 301 and 308, 304, 404.
A request is lines of text, then an empty line: the response adds a status line, headers and a body.

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.1the method, the target and the version
Host: api.acme.examplethe name the client asked for, since one server address can carry many names
Accept: application/jsonthe 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 OKthe version, the status code and its reason
Content-Type: application/jsonthe format of the body
Content-Length: 1840the 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=60how long a cache may reuse the response: 60 s
(an empty line)the end of the headers
the body1,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).

MethodDoesSafeIdempotent
GETreads the targetyesyes
HEADthe same as GET, headers onlyyesyes
POSTsends data for the server to process, often to create somethingnono
PUTreplaces what is at the targetnoyes
DELETEremoves itnoyes
CodeMeans (RFC 9110 sec. 15; 429: RFC 6585 sec. 4)
200 OKit worked
301, 308moved 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 Modifiedthe client’s cached copy is still good; the response has no body
400 Bad Requestthe server could not read the request
401 Unauthorized, 403 Forbidden401: no valid credentials; 403: understood, and refused
404 Not Foundnothing at that target
429 Too Many Requeststhe client sent too many requests in a given time; the response may say when to retry (Retry-After)
500 Internal Server Errorthe server failed
502 Bad Gatewaya proxy got a bad answer from the server behind it
503 Service Unavailableoverloaded or in maintenance, for now
504 Gateway Timeouta 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?

A ladder between the worker and the api, 50 ms per round trip. Cold call: SYN and SYN-ACK (TCP 1 RTT); ACK with ClientHello, then ServerHello to Finished (TLS 1 RTT); Finished with GET, then 200 OK (request 1 RTT), at 150 ms. The connection stays open, so the kept call is GET /orders?page=3 and 200 OK, 50 ms. A panel holds the round-trip table.
A new connection costs two round trips before the request: a kept one costs none.
StepMessagesCold callOn a kept connection
TCP handshake (Part 3)SYN, SYN-ACK, ACK1 RTTnone
TLS 1.3 handshake (Part 8)ClientHello; ServerHello to Finished; Finished1 RTTnone
request and responseGET, 200 OK1 RTT1 RTT
total at 50 ms150 ms50 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.

Four phases on one connection, each kept as a lane of frames. HTTP/1.1: each request waits for its response. HTTP/2: streams 1, 3 and 5 leave together and their frames come back interleaved. HTTP/2 with a lost packet: a frame of stream 3 is lost, and the frames of 1 and 5 after it are held until it is resent. HTTP/3 over QUIC: the same loss holds only stream 3.
HTTP/2 interleaves streams on one connection: one lost packet still holds them all, unless QUIC carries them.

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.1HTTP/2HTTP/3
runs onTCPTCPQUIC, over UDP
requests at once on one connectiononemany, as streamsmany, as streams
a lost packet holds upthe connectionevery stream of the connectiononly its own streams
share of requests to Cloudflare in 202529 %, with HTTP/1.050 %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.

A ladder between the worker and the api, in three phases. At 0 s: GET, then 200 OK, ETag v7, max-age=60, 1,840 bytes. At 75 s the copy is stale: GET with If-None-Match v7, then 304 Not Modified, no body. At 140 s the api’s version is v8: the same GET gets 200 OK, ETag v8. A panel inside the worker keeps one row per phase: stored, fresh again with 0 body bytes, replaced.
A stale copy is checked, not fetched: 304 means keep yours, and no body crosses the wire.

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
#

WhereKind (RFC 9111 sec. 1)ServesWhat it can read
inside the client: a browser, or an HTTP library set up with a cacheprivate cache, “a component of a user agent”one userits own user’s responses
a reverse proxy or a CDN in front of the apishared cacheevery clientevery 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 useshared cachethe clients behind itplain 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
#

CaseDescriptionCheck withWhyFix
a host moved, clients did notthe api has a new address, but some clients still call the old onedig api.acme.example on a client’s resolver: the old address and the TTL leftresolvers keep the old A record until its TTL runs outlower the TTL at least one old TTL before the move, then change the record
a new name still failsa record was just created, and lookups still say it does not existdig: status NXDOMAIN, and the SOA in the authority section with its TTLthe resolver cached the negative answer for the lower of the SOA’s TTL and its minimum fieldwait it out, or clear the resolver’s cache; create records before anything looks them up
large DNS answers failsmall lookups work; a name with many records or signatures times outdig +ignore shows the tc flag on the UDP answer; dig +tcp then times outthe answer is too big for UDP, and a firewall drops TCP port 53allow TCP 53 between clients and resolvers, and from resolvers to the internet
a pooled connection dies after idlingthe first request after a quiet spell fails; a retry on a new connection worksthe time since the connection’s last use, against the NAT’s idle timeouta 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 defaultclose 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 answerthe api changed, and clients still get the old pagecurl -v --http1.1: the < Age and < Cache-Control linesmax-age is too long, or a CDN in front still holds a fresh copyshorten 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-age runs out, then asks with the ETag, and 304 means nothing to download.

Sources
#

Checked 2026-10-16.

Networking foundations - This article is part of a series.
Part 2: This Article