protocol
DNS Message Format
The wire format of DNS — headers, questions, answers, and how they fit into 512 bytes
Every DNS message is the same shape
Queries and responses use an identical structure. A DNS message is a binary format — not JSON, not XML, not protocol buffers. It is a tightly packed sequence of fields designed in 1987 to fit into a single UDP datagram. The format has survived four decades because it is fast to parse, compact on the wire, and simple enough to implement in any language.
Every DNS message consists of five sections, always in the same order:
+---------------------+
| Header | 12 bytes, always present
+---------------------+
| Question | What is being asked
+---------------------+
| Answer | Resource records answering the question
+---------------------+
| Authority | Nameserver records for delegation
+---------------------+
| Additional | Extra records (glue, OPT, TSIG)
+---------------------+ The Header is always exactly 12 bytes. The other four sections can be empty — and in a query, most of them are.
The header: 12 bytes that control everything
The 12-byte header is the control plane of every DNS message:
| Offset | Field | Size | Purpose |
|---|---|---|---|
| 0–1 | ID | 16 bits | Transaction identifier — the client picks a random value, the server echoes it back |
| 2–3 | Flags | 16 bits | QR, Opcode, AA, TC, RD, RA, Z, AD, CD, RCODE |
| 4–5 | QDCOUNT | 16 bits | Number of questions (almost always 1) |
| 6–7 | ANCOUNT | 16 bits | Number of answer records |
| 8–9 | NSCOUNT | 16 bits | Number of authority records |
| 10–11 | ARCOUNT | 16 bits | Number of additional records |
The Flags field packs multiple subfields into 16 bits:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z|AD|CD| RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ The most important flags:
- QR (1 bit): 0 = query, 1 = response. The single bit that distinguishes a question from an answer.
- RD (Recursion Desired): Set by clients that want the resolver to chase referrals on their behalf. Almost always 1.
- RA (Recursion Available): Set by resolvers that support recursion. Tells the client it can rely on this server to do the work.
- AA (Authoritative Answer): Set when the responding server is authoritative for the zone. A response from Cloudflare’s authoritative DNS will have AA=1; a response from Google’s 8.8.8.8 resolver will have AA=0.
- TC (Truncated): The response was too large for UDP. The client should retry over TCP.
- AD (Authentic Data): The resolver validated this response with DNSSEC.
- RCODE (4 bits): The response status — 0 (NOERROR), 3 (NXDOMAIN), 2 (SERVFAIL), and others.
The ID field deserves attention. It is only 16 bits — 65,536 possible values. An attacker who can guess the transaction ID and source port can forge a response before the real one arrives, poisoning the resolver’s cache. This weakness was the basis of the Kaminsky attack in 2008, which led to universal deployment of source port randomization as a second layer of unpredictability.
The question section
The question section describes what the client is asking for. Despite the QDCOUNT field allowing multiple questions, every DNS implementation in practice sends exactly one question per message.
A question has three fields:
| Field | Size | Description |
|---|---|---|
| QNAME | Variable | The domain name being queried |
| QTYPE | 16 bits | Record type (A=1, AAAA=28, MX=15, etc.) |
| QCLASS | 16 bits | Almost always IN (Internet, value 1) |
QNAME uses DNS wire format for domain names: each label is preceded by a length byte, and the name terminates with a zero byte. www.example.com becomes:
03 77 77 77 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00
3 w w w 7 e x a m p l e 3 c o m (root) The maximum label length is 63 bytes (since the length byte uses 6 bits; the top 2 bits are reserved for compression pointers). The maximum total domain name length is 255 bytes in wire format, or 253 characters in dotted text representation.
Resource records: the answer format
The Answer, Authority, and Additional sections all contain resource records (RRs). Every resource record has the same structure:
| Field | Size | Description |
|---|---|---|
| NAME | Variable | The domain name this record applies to |
| TYPE | 16 bits | Record type (A, AAAA, MX, NS, etc.) |
| CLASS | 16 bits | Almost always IN (1) |
| TTL | 32 bits | Time to live in seconds |
| RDLENGTH | 16 bits | Length of RDATA in bytes |
| RDATA | Variable | The record data (depends on TYPE) |
The TTL is a signed 32-bit integer per RFC 2181, giving a maximum value of 2,147,483,647 seconds — about 68 years. In practice, TTLs range from 60 seconds (for fast failover) to 86,400 seconds (24 hours, common for stable records). The TTL tells resolvers how long they can cache this record before asking again.
RDATA varies by record type. An A record’s RDATA is exactly 4 bytes (an IPv4 address). An AAAA record is 16 bytes. An MX record contains a 2-byte preference value followed by a domain name. A TXT record contains one or more character strings, each prefixed with a length byte.
Name compression: saving bytes by pointing backward
DNS messages frequently repeat domain names — the question section contains www.example.com, and the answer section contains it again. Name compression avoids this waste.
A compression pointer replaces a domain name (or suffix) with a 2-byte reference to an earlier occurrence in the same message. The pointer is identified by its two high bits being set to 11:
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1 1| OFFSET |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ The 14-bit offset points to the position in the message where the name (or name suffix) was first written. For example, c0 0c means “the name starting at byte offset 12” — which is typically the beginning of the QNAME in the question section.
Compression is elegant but has been a source of security vulnerabilities. The NAME:WRECK vulnerabilities (2021) exploited malformed compression pointers in embedded TCP/IP stacks, causing buffer overflows in over 100 million IoT devices. Robust parsers must limit pointer-following depth and validate that offsets point backward within the message boundaries.
Walking through a real query and response
A query for www.example.com A record — 45 bytes of DNS payload:
Header (12 bytes): Transaction ID 0xABCD, flags 0x0100 (QR=0, RD=1), QDCOUNT=1, ANCOUNT=0, NSCOUNT=0, ARCOUNT=1 (the OPT record for EDNS).
Question (21 bytes): QNAME=www.example.com, QTYPE=A (1), QCLASS=IN (1).
Additional (11 bytes): An OPT pseudo-record signaling EDNS support with a 4096-byte UDP buffer.
The response echoes the header with QR=1 and RA=1, repeats the question, and adds an answer section:
c0 0c → NAME: pointer to offset 12 (www.example.com)
00 01 → TYPE: A
00 01 → CLASS: IN
00 00 54 60 → TTL: 21,600 seconds (6 hours)
00 04 → RDLENGTH: 4
5d b8 d8 22 → RDATA: 93.184.216.34 The compression pointer c0 0c saves 17 bytes by referencing the domain name already present in the question section. In a response with multiple records for the same name, this saving multiplies.
The 512-byte constraint and what broke it
RFC 1035 specified that DNS messages over UDP must not exceed 512 bytes. This limit was chosen because 576 bytes was the minimum IP datagram size that all hosts were required to handle (RFC 791), and 512 bytes left room for IP and UDP headers.
For the first decade of DNS, 512 bytes was sufficient. But several developments pushed responses beyond this limit:
- DNSSEC signatures (RRSIG records) can easily be 256+ bytes each
- Large delegations with many nameservers
- TXT records for email authentication (SPF, DKIM) growing longer as policies become more complex
- IPv6 glue records doubling the size of referral responses
When a response exceeded 512 bytes, the server set the TC (truncated) flag, and the client had to retry over TCP — adding a full round trip of latency. This was a performance penalty that the DNS community tolerated for years before EDNS provided a better solution.
Response codes: what the numbers mean
The 4-bit RCODE field in the header communicates the outcome of a query:
| RCODE | Name | Meaning |
|---|---|---|
| 0 | NOERROR | Success. If the answer section is empty, the name exists but has no records of the requested type (a condition called NODATA). |
| 1 | FORMERR | The server could not parse the query. Often indicates an EDNS incompatibility. |
| 2 | SERVFAIL | The server encountered an internal error — DNSSEC validation failure, authoritative timeout, or lame delegation. |
| 3 | NXDOMAIN | The domain name does not exist. The strongest negative assertion in DNS. |
| 4 | NOTIMP | The server does not support the requested opcode. |
| 5 | REFUSED | The server refuses the query, typically for policy reasons (e.g., recursion not available to this client). |
EDNS extends the RCODE to 12 bits, enabling additional codes like BADVERS (16), BADCOOKIE (23), and others used for TSIG authentication and dynamic updates.
The distinction between NXDOMAIN and NODATA is subtle but critical. NXDOMAIN means the name itself does not exist — no records of any type. NODATA (NOERROR with an empty answer) means the name exists but not with the requested type. Confusing the two can break wildcard matching and DNSSEC validation.