dnsperf(1)

✨ dnsperf β€” the manual, but make it cute ✨

okay bestie, so you found the dnsperf(1) page in the General Commands Manual and you're like "what is this". don't worry, i got you. this is part 1, so buckle up πŸ’–


🌸 NAME 🌸

dnsperf β€” test the performance of a DNS server. that's it. that's the whole vibe. she's here to make your DNS server sweat πŸ”₯


πŸŽ€ SYNOPSIS πŸŽ€

dnsperf [-a local_addr] [-b bufsize] [-B] [-c clients] [-d datafile]
[-D] [-e] [-E code:secret] [-f family] [-h] [-j] [-l limit] [-m mode]
[-n runs_through_file] [-p port] [-q num_queries] [-Q max_qps]
[-s server_addr] [-S stats_interval] [-t timeout] [-T threads] [-u] [-v]
[-W] [-x local_port] [-y [alg:]name:secret] [-O option=value]

yes that's a lot of flags. we love a girl with options πŸ’…


πŸ¦‹ DESCRIPTION πŸ¦‹

so dnsperf is a DNS server performance testing tool, periodt. her main character energy is aimed at measuring how fast authoritative DNS servers go, but she can also measure caching server performance β€” only if you're doing it in a closed laboratory environment tho πŸ™ˆ. if you wanna test caching servers that resolve against the live Internet, then resperf is the program you actually want. different girl, different job.

now listen, big brain moment: you really should run dnsperf and the name server you're testing on separate machines. why? because if dnsperf itself is hogging the CPU on the same box, she's lowkey slowing down the name server and that ruins the whole point 😭. and those two machines should be connected with a fast network β€” preferably a dedicated Gigabit Ethernet segment (iconic, slay). testing through a router or firewall? don't. just don't. not advisable, bestie.

🧚 Configuring the name server 🧚

if you're using dnsperf to test an authoritative server, set that name server up so it serves one or more zones that are similar in size and number to what the server is expected to serve in production. realistic vibes only ✨

also!! be sure to turn off recursion in the server's configuration. in BIND 8/9, that means specifying recursion no; in the options block. and in BIND 8 you should also specify fetch-glue no; β€” otherwise the server may try to go fetch glue information from the Internet during your test, which slows it down by an unpredictable factor. unpredictable?? we don't know her. no thank you πŸ’€

πŸ’œ Constructing a query input file πŸ’œ

a dnsperf input file should contain a large and realistic set of queries β€” like, on the order of ten thousand to a million. big file, big dreams. the input file has one line per query, and each line is a domain name and an RR type name, separated by a space. the class of the query is implicitly IN (iykyk).

now, when you're measuring performance serving non-terminal zones like the root zone or TLDs, note that those servers spend most of their time giving out referral responses, not authoritative answers. so a realistic input file might be mostly queries for type A for names below, not at, the delegations present in the zone.

for example: say you're testing a server configured to be authoritative for the top-level domain fi., which has delegations for domains like helsinki.fi and turku.fi. then your input file could contain lines like

www.turku.fi A
www.helsinki.fi A

where that www prefix makes sure the server responds with a referral 😌. ideally you'd mix in a realistic proportion of queries for nonexistent domains alongside the existing ones, and the lines of the input file should be in a random order. shuffle that thing, bestie 🎲

want to use a space, or a dot, or literally any other character in the domain name? you can quote them using \DDD where each D is a digit and it's the octet corresponding to the decimal number described by DDD β€” as per RFC1035 5.1. Format, no cap. dots can be quoted without converting, for example:

space\032space.com A
tab\009tab.com A
dot\.dot.com A

🌈 Constructing a dynamic update input file 🌈

to test dynamic update performance, you run dnsperf with the -u option, and the input file gets built out of blocks of lines describing dynamic update messages. the first line in a block contains the zone name:

example.com

after that, the lines contain prerequisites β€” if there are any. prerequisites can say that a name may or may not exist, an rrset may or may not exist, or an rrset exists and its rdata matches all specified rdata for that name and type. the keywords require and prohibit are followed by the appropriate information. all relative names are considered to be relative to the zone name. here are the 5 types of prerequisites:

require a
require a A
require a A 1.2.3.4
prohibit x
prohibit x A

then the subsequent lines contain records to be added, records to be deleted, rrsets to be deleted, or names to be deleted. the keywords add or delete come first, followed by the appropriate information. all relative names are considered relative to the zone name (again, consistent queen πŸ‘‘). here are the 4 types of updates:

add x 3600 A 10.1.2.3
delete y A 10.1.2.3
delete z A
delete w

and every update message gets terminated by a line containing the command:

send

that send is the little "go bestie go" at the end of each block πŸ’Œ

⭐ Running the tests ⭐

when you run dnsperf, a data file (the -d option) and a server (the -s option) will normally be specified. the output of dnsperf is mostly self-explanatory, so you can just read it and vibe.

but pay attention to the number of dropped packets reported 🚨 β€” when you're running the test over a local Ethernet connection, it should be zero. if one or more packets has been dropped, there may be a problem with the network connection. in that case, the results should be considered suspect and you should repeat the test. repeat it. do not trust those numbers, they're sus fr fr.

πŸ’… Using DNS-over-HTTPS πŸ’…

when you're using DNS-over-HTTPS you must set -O doh-uri=... to something that actually works with the server you're sending to. also note that the value for maximum outstanding queries will be used to control the maximum concurrent streams within the HTTP/2 connection. that's the tea β˜•


okay that's part 1 bestie, more coming πŸ’–

✨ OPTIONS ✨

okay bestie, this is the part where dnsperf hands you the whole remote control πŸŽ›οΈ β€” every flag, every knob, every little "actually can you…". Scroll with me, don't skip, they all matter.

  • -a local_addr β€” picks the local address your requests get sent from. Leave it out and you get the wildcard address, no thoughts head empty 🫧
  • -b bufsize β€” sets the socket's send and receive buffer size, measured in kilobytes. If you don't say anything, your operating system's default just does its thing. 🧸
  • -B β€” tells dnsperf to read the datafile in TCP-stream binary format, exactly as laid out by RFC 1035 in the "4.2.2. TCP usage" section. Here's the tea: every packet is preceded by a 2-byte preamble giving the length of the DNS packet that follows, in network byte order, and then immediately the raw bytes of that packet. Those first two bytes of any packet should hold the message ID, and dnsperf overwrites them on the fly ✍️. All the other bytes stay exactly as they are. Packets shorter than two bytes get sent intact. The packets in your datafile can be any arbitrary bytes β€” nobody checks them for validity. And no cap: malformed packets probably won't get responded to by servers, and that means timeouts 😭. This option is mutually exclusive with -D, -e, -E, -u and -y.

You can generate these binary datafiles with any old TCP listener, like netcat (nc) or sockat. TCP is the move here because that's what gets the length prepended. This example shows you how to build a datafile with two queries and then use it β€” you'll need two terminals:

(terminal 1) $ nc -l 127.0.0.1 5300 > dns.blob
(terminal 2) $ echo "example.com A" | dnsperf -s 127.0.0.1 -p 5300 -m tcp
(terminal 1) $ nc -l 127.0.0.1 5300 >> dns.blob
(terminal 2) $ echo "example.com AAAA" | dnsperf -s 127.0.0.1 -p 5300 -m tcp
(terminal 1) $ dnsperf -B -d dns.blob -s $IP
  • -c clients β€” go full main-character and act as multiple clients. Requests get sent from multiple sockets. Default? Just the one client, living its single-socket life. πŸ¦‹
  • -d datafile β€” your input data file. Not specified? dnsperf just reads from standard input like a little gremlin. πŸ“
  • -D β€” sets the DO (DNSSEC OK) bit [RFC3225] in every packet that gets sent. This also switches on EDNS0, which DNSSEC literally requires. Mutually exclusive with -B. 🫢
  • -e β€” enables EDNS0 [RFC2671] by slapping an OPT record onto all the packets sent. Also mutually exclusive with -B.
  • -E code:value β€” adds an EDNS [RFC2671] option to every packet sent, using the numeric option code you give and the value as a hex-encoded string. This also enables EDNS0. And yes, mutually exclusive with -B. πŸ’…
  • -f family β€” says which address family is used for sending DNS packets. Possible values: "inet", "inet6", or "any". If "any" (which is the default) is chosen, dnsperf picks whichever address family actually fits the server it's sending to. Adaptive icon behavior ✨
  • -h β€” prints a usage statement and dips. πŸ‘‹
  • -j β€” NOTE: Only available if built-in!

Outputs streaming JSON objects for progress and statistics. Full details live in the JSON OUTPUT section. Behold:

$ src/dnsperf -d ../src/test/datafile -s 172.17.0.1 -j -S1 -Q1 -n 2
{"start":{"version":"2.14.0","command_line":["dnsperf","-d","../src/test/datafile","-s","172.17.0.1","-j","-S1","-Q1","-n","2"],"queries":true,"server_addr":"172.17.0.1","server_port":53,"started":"Thu Dec 18 08:41:05 2025","maxruns":2}}
{"rate":{"timestamp_usec":1766047266411099,"qps":0.99872063886161822}}
{"rate":{"timestamp_usec":1766047267412462,"qps":1.997277710480615}}
{"rate":{"timestamp_usec":1766047268413677,"qps":0.99878647443356328}}
{"stop":{"reason":"end of file"}}
{"statistics":{"sent":4,"completed":4,"lost":0,"request_avg_pktsize":28,"response_avg_pktsize":50,"run_time":4.0,"qps":0.99992450569981972,"latency":{"avg":0.02145,"min":0.00026400000000000002,"max":0.042937000000000003,"stddev":0.024424912669581168}}}

It plays nicely with the extended options verbose-interval-stats and latency-histogram. Currently this option is not compatible with -v or -W though.

  • -l limit β€” sets a time limit for the run, in seconds. Heads up: this can make the input get read multiple times, or only part of it get read. Default behavior is to read the input once with no specific time limit. ⏳
  • -m mode β€” the transport mode, bestie. Options are "udp", "tcp", "dot" or "doh". Default is "udp". πŸš€
  • -n runs_through_file β€” run through the input file at most this many times. If there's no time limit set, the file gets read exactly this number of times. If there is a time limit, the file may get read fewer times. Math is math. 🀷
  • -p port β€” sets the port the DNS packets go out on. Not specified? You get the standard DNS port (udp/tcp 53, DoT 853, DoH 443). πŸŽ€
  • -q num_queries β€” the max number of outstanding requests. Once you hit that number, dnsperf stops sending new requests until responses come in or requests time out. Default is 100. 🌈
  • -Q max_qps β€” caps the number of requests per second. There's no default limit β€” go feral (responsibly). πŸ”₯
  • -s server_addr β€” the name or address of the server receiving your requests. Default is the loopback address, 127.0.0.1. Home sweet home. 🏠
  • -S stats_interval β€” if you set this, dnsperf prints a count of the number of answers received per second during the interval, every stats_interval seconds. Constant little progress updates, we love her. πŸ“ˆ
  • -t timeout β€” the request timeout value, in seconds. After this many seconds have gone by, dnsperf stops waiting for a response to that particular request. Default is 5 seconds. ⏰
  • -T threads β€” run multiple client threads. By default dnsperf uses one thread for sending requests and one thread for receiving responses. Specify this option and it switches to using N pairs of send/receive threads instead. Teamwork makes the dream work. πŸ‘―
  • -u β€” tells dnsperf to send DNS dynamic update messages instead of queries. The input file format is different for this, so go read the "Constructing a dynamic update input file" section for the deets. Mutually exclusive with -B. πŸ“
  • -v β€” verbose mode, the yapper era. The DNS RCODE of every response gets reported to standard output when the response arrives, latency included. Query times out? It's reported with the special string "T" instead of a normal DNS RCODE. Query gets interrupted? Special string "I". Extra info about network readiness and congestion gets reported too. πŸ—£οΈ
  • -W β€” logs warnings and errors to standard output instead of standard error, so scripts, tests and automation can catch all the output in one place. Convenient king behavior. πŸ‘‘
  • -x local_port β€” the local port requests get sent from. Default is the wildcard port (0).

And here's the nuance: if you're acting as multiple clients and the wildcard port is used, each client gets its own different random port. But if you specify a port, the clients use a range of ports starting with the specified one. 🎯

  • -y [alg:]name:secret β€” adds a TSIG record [RFC2845] to all packets sent, using the TSIG key algorithm, name and secret you specify. The algorithm defaults to hmac-md5, and the secret is expressed as a base-64 encoded string. Available algorithms are: hmac-md5, hmac-sha1, hmac-sha224, hmac-sha256, hmac-sha384 and hmac-sha512. Mutually exclusive with -B. πŸ”
  • -O option=value β€” sets an extended long option for all sorts of things that control different aspects of testing or the protocol modules. The full menu of available options is in EXTENDED OPTIONS. 🧚

next up: more sections, keep scrolling bestie, we're not done yet πŸ’–

✨ EXTENDED OPTIONS ✨

okay so these are the extended options, and they are lowkey the extra-spicy controls, bestie. ✨

  • doh-uri=URI β€” the URI to use for DNS-over-HTTPS. The default value is "https://localhost/dns-query". 🫢
  • doh-method=HTTP_METHOD β€” the HTTP method to use when querying with DNS-over-HTTPS. Default is GET. Available methods are: GET, POST. πŸ’…
  • tls-sni=SERVER_NAME β€” the Server Name Indication (SNI) to use for TLS connections (such as DNS-over-TLS or DNS-over-HTTPS). It defaults to leaving out the SNI extension in the client hello. πŸŽ€
  • suppress=MESSAGE[,MESSAGE,...] β€” suppress various messages and warnings that may be shown excessively in some situations, like socket readiness when you're connecting to a slow service. You can suppress multiple types by listing them as a comma-separated list. These types are available:
    • timeouts β€” suppress messages about queries being timed out.
    • congestion β€” suppress messages about network congestion.
    • sendfailed β€” suppress messages about failure to send packets, or if only parts of the packet were sent.
    • sockready β€” suppress messages about socket readiness.
    • unexpected β€” suppress messages about answers with an unexpected message ID.
  • num-queries-per-conn=NUMBER β€” this limits the number of queries sent over a connection before triggering a re-connection. Once re-connected, it resets the counter and keeps sending queries until the limit is reached again, triggering another re-connection, and so on. Using this option also enables counting the number of responses received for each connection. Once the limit is reached for sending queries, it waits until the same amount of responses has been received before re-connecting. Waiting for responses may timeout, and the timeout used for this is the same as specified by -t. Note that this option is only useful for connection oriented protocols. πŸ”Œ
  • verbose-interval-stats β€” change the statistics format of -S to that shown at end of run. πŸ“Š
Please note: min/max values for latency and connections are not available in interval statistics. The number of answers received within stats_interval can legitimately exceed the number of queries sent, depending on answer latency, configured timeout, and stats_interval.

Only available in dnsperf.

  • latency-histogram β€” print detailed latency histograms for DNS answers and connections. Latency is quantized into bins with roughly 3 % resolution, and the latency range for individual bins increases logarithmically. This is done to limit the amount of memory required for histograms, and it also allows you to visualize latency using logarithmic percentile histograms with minimal postprocessing. 🌈 Only available in dnsperf and if compile time support was detected.
  • qps-threshold-wait=<microseconds> β€” when using -Q to rate limit queries sent, this option controls the minimum time (in microseconds) there should be between queries to use nanosleep() to wait until sending the next query. Setting it to zero (0) will disable any call to nanosleep() between sending queries. ⏳ If the time between queries is lower than this, then no wait is performed, in order to have more precision on when to send the next query. This is because during high QPS rate limiting, it can take more time just calling the functions to wait for when to send the next query than the actual time between queries. If not set, the average call-time to nanosleep() will be measured during startup. Only available in dnsperf.
  • tls-session-resumption β€” enable TLS session resumption for DNS-over-TLS and DNS-over-HTTPS connections. ✨ This option is only useful for connection-oriented TLS transports and is primarily intended for testing reconnect and session resumption behavior. Only available in dnsperf.
  • tls-sslkeylogfile β€” enable logging of TLS session secrets to the file specified by the SSLKEYLOGFILE environment variable. πŸ” When enabled, TLS secrets are appended to the file pointed to by SSLKEYLOGFILE in NSS key log format. This can be used with tools such as Wireshark to decrypt captured TLS traffic for debugging purposes. If SSLKEYLOGFILE is not set, no key log file will be written. Only available in dnsperf.

πŸ¦‹ JSON OUTPUT πŸ¦‹

Following data is available in the JSON output, and the dot is a delimiter for objects. So like start.version, statistics.sent, etc. β€” iykyk. πŸ’–

  • start.version=string β€” the version of dnsperf that generated the JSON.
  • start.command_line=[string,...] β€” the executed command and its options as an array of strings.
  • start.updates=boolean β€” set to true if what was sent was dynamic updates.
  • start.queries=boolean β€” set to true if what was sent was DNS queries.
  • start.server_addr=string β€” the server address sent to.
  • start.server_port=int β€” the server port sent to.
  • start.started=string β€” date and time when execution started.
  • start.timelimit=double β€” set to a number of seconds if execution was limited to that.
  • start.maxruns=int β€” set to a number of runs if execution was limited to that.
  • rate.timestamp_usec=uint β€” current UNIX timestamp in microseconds.
  • rate.qps=double β€” current queries per second sent.
  • stop.reason=string β€” the reason the execution was stopped.
  • statistics.interval=boolean β€” set to true if this statistics object was outputted as requested by the extended option verbose-interval-stats.
  • statistics.sent=uint β€” the number of queries sent.
  • statistics.completed=uint β€” the number of responses received for queries sent.
  • statistics.lost=uint β€” the number of queries that didn't receive a response, either timed out or by other means.
  • statistics.interrupted=uint β€” the number of queries that was in some way interrupted.
  • statistics.unexpected=uint β€” the number of responses received that did not have a corresponding query.
  • statistics.request_avg_pktsize=uint β€” average packet size for queries.
  • statistics.response_avg_pktsize=uint β€” average packet size for responses.
  • statistics.runtime=double β€” the run time in seconds.
  • statistics.qps=double β€” the queries per second.
  • statistics.latency=latency β€” a query/response latency object, see below.
  • statistics.connection.attempts=uint β€” the number of connection attempts made. Only available when using connection oriented protocols.
  • statistics.connection.completed=uint β€” the number of connections established. Only available when using connection oriented protocols.
  • statistics.connection.latency=latency β€” a connection latency object, see below. Only available when using connection oriented protocols.
  • dns_over_https.status_codes={ http_status_code: count, ... } β€” specific statistics when DNS-over-HTTPS protocol is used. This is an object with a counter for each of the HTTP Status codes received.
  • dns_over_https.tls_sessions_resumed=uint β€” the number of TLS sessions resumed on a reconnect for DNS-over-HTTPS when using tls-session-resumption.
  • dns_over_tls.tls_sessions_resumed=uint β€” the number of TLS sessions resumed on a reconnect for DNS-over-TLS when using tls-session-resumption.
  • latency.avg=double β€” average latency.
  • latency.min=double β€” minimum latency.
  • latency.max=double β€” maximum latency.
  • latency.stddev=double β€” standard deviation of the latency.
  • latency.histogram=histogram β€” a histogram of the latency, see below.
  • histogram=[[min, max, count],...] β€” a histogram object, see extended option latency-histogram for more information.

πŸ‘€ SEE ALSO πŸ‘€

πŸ’– AUTHOR πŸ’–

πŸ’€ BUGS πŸ’€

For issues and feature requests please use:

For question and help please use:

  • admin@dns-oarc.net

This whole page is giving dnsperf 2.16.0 and it's the dnsperf(1) manual page energy. Byeeee, bestie. πŸ’…βœ¨πŸ¦‹

πŸ“¦ where this page comes from