Every DNS lookup is a short message, usually a single UDP datagram, and every answer comes back in exactly the same format. This chapter takes both apart: the 12-byte header and its flags, the four sections, the trick that squeezes repeated names, and what happens when an answer is too big to fit. If ports, UDP and TCP are new to you, start with networks · TCP, UDP & ports; for what DNS is for and the "UDP, size limits and privacy" basics, see networks · DHCP & DNS. Here we open the envelope.
Common mix-up: 512 bytes is not the largest DNS answer. It is the largest answer a server may send over plain UDP to a client that never said it could take more. A client that uses EDNS can accept a bigger datagram, and over TCP a message can be up to 65,535 bytes, because its length travels in a 2-byte prefix. When an answer does not fit, the server does not fail: it sets the TC bit, and the client asks again over TCP.
When your laptop asks for www.example.com, the bytes that leave it are not text. There is no "GET", no colon, no line break. There is a binary structure that Paul Mockapetris laid out in RFC 1035 in November 1987, and that every DNS server and client since has had to read the same way. The design has aged remarkably well: the extensions that came later (DNSSEC, EDNS, extended errors) all fit inside the original frame instead of replacing it.
The single most useful fact about that frame is that queries and responses have the same layout. A response is the query handed back with the header changed and answers appended. A server copies your question into its reply word for word, so a client can check that the answer is for the question it asked. One parser reads both directions.
Every message has five parts, always in this order. The header is fixed at 12 bytes. Then come four sections of variable length: the question (what is being asked), the answer (records that answer it), the authority (records that point toward the servers responsible for the name, or prove that it does not exist) and the additional section (records that might save the client another lookup, and, since EDNS, a special pseudo-record carrying options). The header says how many entries each section holds; the sections themselves carry no markers or separators. A parser simply reads that many entries, one after another.
Echoing the question is a cheap integrity check. Over UDP there is no connection, so a reply is matched to its query by three things: the source address and port it came from, the 16-bit ID in the header, and the question section. A client that receives a reply whose question does not match what it sent must throw it away. That matters for security as well as correctness: an off-path attacker trying to slip a forged answer into a resolver's cache has to guess all of those at once. Chapter 13 covers that contest; the defense depends on these bytes being checked.
In theory QDCOUNT can be more than 1. In practice almost no server accepts more than one question per message, because the header has only one RCODE and one AA bit: there is no way to say "the first question failed and the second one is authoritative". Treat one question per message as the rule.
The header is six 16-bit numbers. Four of them are counts, one is an identifier, and one, the second word, is packed with eleven separate fields. Here they are in order.
ID (16 bits) is a number the client picks. The server copies it into the reply, and the client uses it to pair replies with outstanding queries. A modern client picks it at random for every query, because a predictable ID makes forged replies easier to land.
QR (1 bit) is 0 in a query and 1 in a response. OPCODE (4 bits) says what kind of message this is: 0 is an ordinary query, and almost everything you will ever see is opcode 0. NOTIFY (4) and UPDATE (5) are used between servers and by tools that edit zones; Chapter 6 covers them.
Then four one-bit flags. AA, authoritative answer, is set by a server that owns the zone the name lives in; a resolver answering from its cache leaves it clear. TC, truncated, means the message was cut short because it did not fit; you will meet it again below. RD, recursion desired, is set by a client that wants the server to chase the answer through the tree on its behalf; your laptop sets it, and a resolver talking to authoritative servers usually clears it. RA, recursion available, is the server saying it offers that service.
The next three bits were a single 3-bit "Z, reserved, must be zero" field in 1987. Two of them were later claimed by DNSSEC (RFC 4035). AD, authentic data, is set by a validating resolver when every record in the answer checked out cryptographically. CD, checking disabled, is set by a client that wants the resolver to hand over the data without validating it, usually because the client will validate it itself. One bit, still called Z, remains reserved and must be zero.
The last four bits are the RCODE, the response code: 0 NOERROR, 1 FORMERR (the server could not parse your query), 2 SERVFAIL (the server tried and failed), 3 NXDOMAIN (the name does not exist), 4 NOTIMP (not implemented) and 5 REFUSED (the server will not answer you). Four bits only allow 16 values, which turned out to be too few; EDNS extends the field, as you will see.
| Field | Bits | Set by | Meaning |
|---|---|---|---|
| ID | 16 | client | Random tag; the reply carries the same value |
| QR | 1 | both | 0 = query, 1 = response |
| OPCODE | 4 | client | 0 QUERY, 2 STATUS, 4 NOTIFY, 5 UPDATE, 6 DSO; 1 (IQUERY) is obsolete |
| AA | 1 | server | Answer comes from a server authoritative for the zone |
| TC | 1 | server | Truncated: the full message did not fit; retry over TCP |
| RD | 1 | client | Recursion desired; copied into the reply |
| RA | 1 | server | Recursion available at this server |
| Z | 1 | nobody | Reserved, must be zero |
| AD | 1 | resolver | Authentic data: the answer passed DNSSEC validation (RFC 4035) |
| CD | 1 | client | Checking disabled: do not validate for me (RFC 4035) |
| RCODE | 4 | server | 0 NOERROR, 1 FORMERR, 2 SERVFAIL, 3 NXDOMAIN, 4 NOTIMP, 5 REFUSED … |
| QD/AN/NS/AR COUNT | 16 each | both | Number of entries in the question, answer, authority and additional sections |
The AD bit is only as trustworthy as the path it traveled. A stub on your laptop that sees AD=1 from a resolver across an untrusted Wi-Fi network has no way to know the bit was not flipped on the way: the header is not signed. RFC 4035 and its clarifications in RFC 6840 say a stub should only rely on AD when it trusts the resolver and the channel to it, for example a resolver on the same machine, or one reached over an encrypted transport (Chapter 12). RFC 6840 also lets a client set AD in a query to say "tell me whether you validated", without asking for DNSSEC records themselves.
The bit-numbering in RFCs can trip you up: RFC 1035 draws the flags word with bit 0 on the left, the most significant end. So when a document says "bit 10" it may be counting from the left. The instrument below avoids the problem by labeling every bit by name.
These are real header bytes. Click any of the 16 flag bits to toggle it, change the ID, opcode, response code or counts, or type 24 hex digits straight into the box. Every control, the hex and the plain-English reading underneath are recomputed from the same 12 bytes, so they always agree. Try the presets, then set the Z bit and see why a strict server answers FORMERR.
Type or paste exactly 24 hex digits; spaces are ignored.
Flags word, most significant bit first. Blue underline: OPCODE bits. Green underline: RCODE bits.
A question has three fields. QNAME is the name being asked about. QTYPE is a 16-bit number for the record type (1 for A, 28 for AAAA, 15 for MX; IANA keeps the list). QCLASS is a 16-bit class, which in practice is always 1, IN, for internet. (Class 3, CH for Chaosnet, survives mostly as a way to ask a server which software it runs.)
Names are not stored with dots. Each label is written as a length byte followed by that many bytes of characters, and the name ends with a zero-length label: a single 0 byte, which stands for the root of the tree from Chapter 1. So www.example.com becomes 3 www 7 example 3 com 0: 17 bytes for a 15-character name. Because the length byte comes first, a parser never searches for a terminator, and a dot inside a label is just another byte. The rules that fall out of this: a label holds at most 63 bytes, and a whole name, length bytes included, at most 255.
Every record in the answer, authority and additional sections uses one layout, the resource record. Its owner NAME comes first, encoded the same way. Then TYPE and CLASS (2 bytes each), then a 32-bit TTL in seconds (how long a cache may keep it; Chapter 4), then RDLENGTH, a 16-bit count of the bytes that follow, and then the RDATA itself, whose shape depends on the type: four bytes for an A record, sixteen for AAAA, a name for CNAME or NS, a 2-byte preference plus a name for MX. RDLENGTH means a parser can skip any record it does not understand, which is how new record types could be added for decades without breaking old software.
Here is a complete worked example: the query a stub sends for the A record of www.example.com, with ID 0x2A7F, recursion desired, and no EDNS. It is 33 bytes.
; offset bytes meaning 0 2a 7f ID = 10879 2 01 00 flags: QR=0 query, OPCODE 0, RD=1 4 00 01 00 00 00 00 00 00 QDCOUNT 1, AN 0, NS 0, AR 0 12 03 77 77 77 label of 3: "www" 16 07 65 78 61 6d 70 6c 65 label of 7: "example" 24 03 63 6f 6d label of 3: "com" 28 00 the root: end of name 29 00 01 QTYPE 1 = A 31 00 01 QCLASS 1 = IN ; 33 bytes of DNS; + 8 UDP + 20 IPv4 = 61 bytes on the wire
DNS names compare case-insensitively for the ASCII letters A to Z, but the bytes on the wire keep whatever case was sent, and a server echoes the question exactly. Some resolvers exploit that: they randomize the case of each letter in the query (wWw.ExAmPlE.cOm) and reject replies whose question does not match bit for bit. That trick, called "0x20 encoding" after the bit that separates upper from lower case in ASCII, adds a few more bits for a forger to guess.
The wire format itself allows any byte value in a label, including spaces, dots and zero bytes. Host names are much stricter (letters, digits and hyphens), and internationalised names are carried as ASCII "xn--" labels. A careful parser must still handle any byte, which is why dig prints odd characters as \DDD decimal escapes, and why the decoder below does too.
A typical response repeats the same names over and over. The question names www.example.com; the answer's owner is www.example.com again; its CNAME target is web.example.com; the authority section lists example.com and ns1.example.com. With only 512 bytes to play with in 1987, writing each one out in full was a waste. So RFC 1035 added name compression: anywhere a name, or the tail end of a name, has already appeared in the message, the encoder can write a 2-byte pointer to the earlier copy instead.
The trick fits in the length byte. A label length is at most 63, which in binary is 00111111: the top two bits of a real length byte are always 00. If they are 11 instead, the byte is the start of a pointer, and the remaining 14 bits, across this byte and the next, are an offset counted from the first byte of the message. In the response to our query, the question's www.example.com starts at offset 12, so a later copy of it becomes C0 0C: binary 11000000 00001100, "pointer, offset 12". And because example.com starts four bytes later, at offset 16, the CNAME target web.example.com can be written as the label 03 web followed by C0 10. A name is a sequence of labels that may end in a pointer, so whole names and shared suffixes both compress.
Compression saves a lot. In the simulated response in Instrument 2, the six names after the question would take 98 bytes written out; compressed, they take 20, and the whole response shrinks from 190 bytes to 112. But it also hands a parser a hazard. A pointer is an instruction to jump, and nothing in the format stops a malicious or broken message from pointing a name at itself, or two pointers at each other. A naive parser that just follows pointers will loop forever, or build a name until memory runs out. Such a message is a cheap denial-of-service against any server or client that reads it.
The defense is simple and every serious implementation has it: count. Refuse to follow more pointers than a name could legitimately need, refuse any name whose expanded length passes 255 bytes, and refuse a pointer that lands outside the message. Many parsers also insist that a pointer points backwards, to an offset earlier than itself; RFC 1035 describes pointers as referring to a "prior occurrence" of the name, and a backwards-only rule makes loops impossible by construction. The bug class has a name in MITRE's catalog: CWE-835, "Loop with Unreachable Exit Condition". It is a classic trap for anyone writing a DNS parser by hand, and a pointer chain is one of the first things a fuzzer will try.
Compression is allowed in owner names and in the RDATA of the record types defined in RFC 1035 that contain names: NS, CNAME, PTR, MX, SOA and a few others. RFC 3597 later fixed the rule for everything newer: new record types must not use compression inside their RDATA, because a server that does not know a type cannot know where the names in it are, and so could not safely rewrite or relocate them. That is why, for example, the target name inside an SRV or HTTPS record is always written out in full.
Compression is also why a DNS message cannot be cut and spliced at arbitrary points. Move a record and its pointers may now aim at the wrong bytes. Servers build each response from scratch with a table of names already written, which is exactly what the encoder in Instrument 2 does.
Type a name and choose a type to build the exact query bytes a client would send. Each byte is tinted by section and named in the field list; click any byte or field to read it. Then load the simulated response: underlined bytes with an orange border are compression pointers, and clicking one shows where it jumps (dashed outline) and the name it expands to. The decoder is a real parser, so you can also edit the hex and decode it, or load the broken message and watch it refuse a pointer loop. All data here is simulated, using the reserved name example.com and documentation addresses.
RFC 1035 capped a DNS message over UDP at 512 bytes. The figure sits neatly inside a rule IPv4 already had: every host must be able to accept a datagram of at least 576 bytes, and 512 bytes of DNS plus the IP and UDP headers fits within it (RFC 6891 spells out the pairing). It was plenty for an A record or two. It is not plenty for a signed answer with DNSSEC signatures, a TXT record full of email policy, or an IPv6-heavy list of name servers.
When an answer will not fit, the server does not fragment it or give up. It sends what fits (often just the header and the question) with the TC bit set to 1. TC=1 is not an error. It means "this is incomplete; ask me again over TCP". The client opens a TCP connection to the same server on port 53 and sends the same query. Because TCP is a byte stream with no message boundaries, each DNS message on it is preceded by a 2-byte length prefix, so the reader knows where one message ends and the next begins. That prefix is the only difference in the bytes; the message itself is identical. The two bytes also set the ceiling: 65,535 bytes per message.
The cost is time. A UDP lookup is one round trip. A truncated lookup is that round trip, plus one for TCP's handshake, plus one for the query and answer over TCP: three in all, before counting any TLS. How TCP's handshake works is covered on networks · TCP, UDP & ports; what matters here is that the fallback must work. For years, firewalls were configured on the folk belief that "DNS is UDP", and blocked TCP port 53. RFC 7766 (2016) put that to rest: every DNS implementation, authoritative and recursive, MUST support TCP. A server that cannot be reached over TCP will fail for exactly the large answers that need it.
$ dig TXT big.example.com (simulated output) ;; Truncated, retrying in TCP mode. ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10879 ;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1 ;; ...answer section... ;; SERVER: 192.0.2.53#53(192.0.2.53) (TCP) ;; MSG SIZE rcvd: 1844
RFC 7766 also tidied up how TCP should be used: a client may send several queries down one connection without waiting (pipelining), a server may answer them out of order (the ID tells them apart), and connections should be kept open for reuse rather than torn down after each answer. Zone transfers between servers (AXFR and IXFR, Chapter 6) have always used TCP, since a whole zone never fits in a datagram. The encrypted transports in Chapter 12 build on the same framing: DNS over TLS sends exactly these length-prefixed messages inside a TLS session on port 853.
By the late 1990s the header had run out of room: no free flag bits, a 4-bit RCODE, and a 512-byte ceiling. Changing the header would have broken every parser on earth. EDNS, the Extension Mechanisms for DNS (first RFC 2671 in 1999, now RFC 6891 from 2013), found a cleverer way in: a fake record. A client that speaks EDNS adds one OPT pseudo-record to the additional section of its query. Old servers that do not understand type 41 ignore it or answer FORMERR, and the client then knows to fall back. New servers read it, and put an OPT record in their reply too.
The OPT record reuses the ordinary record fields for new purposes. Its NAME is the root, a single 0 byte. Its TYPE is 41. Its CLASS field carries the sender's UDP payload size: "I can receive a DNS message this big over UDP". Its 32-bit TTL field is split into an 8-bit extension of the RCODE (giving 12 bits in all), an 8-bit EDNS version (0 is the only one in use, hence "EDNS(0)"), and 16 flag bits, of which only the top one is assigned: DO, "DNSSEC OK", meaning "please include signatures". Its RDATA holds a list of options, each a 2-byte code, a 2-byte length and the data.
| OPT field | Normal meaning | Meaning in OPT | Example |
|---|---|---|---|
| NAME | Owner name | Always the root (one 0 byte) | 00 |
| TYPE | Record type | 41 = OPT | 00 29 |
| CLASS | Class (IN) | Largest UDP payload the sender can receive | 04 d0 = 1232 |
| TTL | Cache lifetime | Extended RCODE (8) · version (8) · DO + 15 zero bits | 00 00 80 00 = version 0, DO=1 |
| RDLENGTH | Length of RDATA | Length of the option list | 00 0c |
| RDATA | Record data | Options: code, length, value … | COOKIE (10), Padding (12), Client Subnet (8), EDE (15) |
The options are where EDNS keeps growing. DNS Cookies (option 10, RFC 7873) let a client and server recognize each other's later messages, which makes spoofed replies and reflection floods harder. Padding (option 12, RFC 7830) rounds encrypted messages up to fixed sizes so their length does not give away the name. Client Subnet (option 8, RFC 7871) lets a resolver tell a content network roughly where its client is, a privacy trade-off covered in Chapter 9. And Extended DNS Errors (option 15) explain failures, below.
EDNS let clients advertise big buffers, and for years 4096 bytes was the common default. That created a new problem. Every network link has a maximum transmission unit (MTU), typically 1500 bytes on Ethernet and often less through tunnels and VPNs. A 3000-byte UDP answer does not fit one packet, so IP fragments it, and fragments behave badly. Many firewalls and middleboxes drop them. If any fragment is lost the whole datagram is lost, and the client sees nothing at all: no reply, no TC bit, just a timeout. Worse, fragments carry no UDP port and no DNS ID after the first one, so an attacker can try to splice a forged second fragment into a real reply, a cache-poisoning route that the usual ID and port randomization does not protect.
The fix agreed by DNS software vendors and operators on DNS Flag Day 2020 was to stop asking for big UDP answers. They recommended a default EDNS buffer of 1232 bytes: IPv6 requires every link to carry at least 1280 bytes, and 1280 minus a 40-byte IPv6 header and an 8-byte UDP header leaves 1232. An answer that size fits nearly every path without fragmenting. Anything bigger is truncated and fetched over TCP, which handles large messages properly. RFC 9715 (2025) wrote the same advice into an RFC, with a recommended maximum of 1400 bytes and a firm preference for avoiding fragmentation altogether.
Set the size of the answer, the buffer the client advertises and the path's MTU. The lab adds the UDP and IP headers, decides what the server sends (a whole answer, or TC=1), works out whether the packet must be fragmented and into how many pieces, and draws the round trips it takes to get an answer, including the timeout when a firewall eats a fragment. The timeout figure is an assumption for illustration; real resolvers differ.
For most of DNS's life, a failure was a four-bit shrug. SERVFAIL means "something went wrong while I was trying", and it covers an enormous range of causes: an expired DNSSEC signature, every authoritative server timing out, a broken delegation, a resolver policy blocking the name. Operators debugging an outage had to guess, or reproduce the lookup by hand.
Extended DNS Errors (RFC 8914, 2020) put the reason in the message. An EDE is an EDNS option, code 15, carrying a 16-bit INFO-CODE and an optional line of text. It rides alongside the ordinary RCODE rather than replacing it, so old clients see the same SERVFAIL as ever while new tools can print the cause. It can also accompany a successful answer: code 3, "Stale Answer", says the resolver served an expired cache entry because the authoritative servers could not be reached.
| INFO-CODE | Name | What it tells you |
|---|---|---|
| 0 | Other | Catch-all; read the text |
| 3 | Stale Answer | Served from expired cache because the authorities were unreachable |
| 6 | DNSSEC Bogus | Validation failed: the signatures do not check out (Chapter 11) |
| 7 | Signature Expired | The RRSIG's validity period has ended; someone forgot to re-sign |
| 9 | DNSKEY Missing | The parent's DS points at a key the zone does not publish |
| 15 | Blocked | The resolver's operator blocks this name by policy |
| 18 | Prohibited | This client is not allowed to use this resolver |
| 22 | No Reachable Authority | No authoritative server answered |
| 23 | Network Error | A network error reaching an authority |
$ dig A expired.example.com (simulated output) ;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 4321 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 1232 ; EDE: 7 (Signature Expired)
RFC 8914 is careful about trust. An EDE is a diagnostic, not a security signal: it is not signed, it can be forged as easily as any other part of an unprotected reply, and a client must not change its behavior (for example, treat a failure as success) because of one. It tells a human where to look. The full list of codes, which has grown since 2020, is kept by IANA in the DNS parameters registry.