H2LOAD(1)

πŸ’– H2LOAD(1) β€” the nghttp2 girlies' benchmark bestie πŸ’–

okay so real quick, this whole page lives inside the nghttp2 manual, section 1, and the tool is called h2load. that's the vibe. let's go πŸ¦‹


✨ NAME ✨

h2load β€” HTTP/2 benchmarking tool πŸ‹οΈβ™€οΈπŸ’…


✨ SYNOPSIS ✨

h2load [OPTIONS]... [URI]...

✨ DESCRIPTION ✨

bestie. it's literally a benchmarking tool for an HTTP/2 server. she loads, she measures, she slays. 😌πŸ”₯

  • <URI> β€” Specify URI to access. Multiple URIs can be specified. URIs are used in this order for each client. All URIs are used, then first URI is used and then 2nd URI, and so on. The scheme, host and port in the subsequent URIs, if present, are ignored. Those in the first URI are used solely. Definition of a base URI overrides all scheme, host or port values. 🫢
ngl the URI rules are giving "first one is the main character, everyone else is just a vibe" πŸ‘‘ β€” all the other URIs get their scheme, host and port ignored entirely, fr fr. only the first URI's scheme/host/port matter. and if you define a base URI, that overrides all scheme, host or port values. periodt.

✨ OPTIONS ✨

okay bestie, here's the whole menu of flags. there are a LOT. hydrate first. πŸ’–

  • -n, --requests=<N> β€” how many requests total across all the clients. teeny tiny twist though: if you pair it with --timing-script-file, this instead means the number of requests each client does, not the grand total. and heads up, this whole option gets ignored if you're doing timing-based benchmarking (that's the --duration option).

Default: 1

  • -c, --clients=<N> β€” how many concurrent clients you want. if you also pass -r, this is the maximum number of connections that get made.

Default: 1

  • -t, --threads=<N> β€” number of native threads.

Default: 1

  • -i, --input-file=<PATH> β€” path to a file with multiple URIs, separated by EOLs. using this turns off getting URIs from the command line. if <PATH> is '-', URIs come in from stdin instead. each client walks the URIs in order, and when it runs out, it loops back to the first URI, then the 2nd, and so on. the scheme, host and port in the later URIs (if they even have any) get ignored β€” only the ones in the first URI are used. unless you define a base URI, which overrides all scheme, host or port values.
  • -m, --max-concurrent-streams=<N> β€” max concurrent streams to issue per session. when it's http/1.1, this is instead the number of HTTP pipelining requests in-flight.

Default: 1

  • -f, --max-frame-size=<SIZE> β€” the biggest frame size the local endpoint is willing to receive.

Default: 16K

  • -w, --window-bits=<N> β€” sets the stream level initial window size to (2**<N>)-1. for QUIC, <N> gets capped at 26 (roughly 64MiB). it defaults to 24 (16MiB) for QUIC, and 30 for everything else.
  • -W, --connection-window-bits=<N> β€” sets the connection level initial window size to (2**<N>)-1. ✨

Default: 30

  • -H, --header=<HEADER> β€” add a header to the requests, or override one that's already there.
  • --ciphers=<SUITE> β€” set the allowed cipher list for TLSv1.2 or earlier. the format of the string is described in OpenSSL ciphers(1).

Default:

ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
  • --tls13-ciphers=<SUITE> β€” set the allowed cipher list for TLSv1.3. format of the string is described in OpenSSL ciphers(1).

Default:

TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_CCM_SHA256
  • -p, --no-tls-proto=<PROTOID> β€” specify the ALPN identifier of the protocol to use when you're hitting an http URI without SSL/TLS. the available protocols are: h2c and http/1.1.

Default: h2c

  • -d, --data=<PATH> β€” POST the FILE to the server. this flips the request method to POST. for an http/1.1 connection, if -d is used, the maximum number of in-flight pipelined requests gets set to 1.
  • -r, --rate=<N> β€” the fixed rate at which connections get created. the rate has to be a positive integer, and it represents how many connections get made per rate period. the max number of connections made is whatever you gave in -c. this rate gets spread across threads as evenly as possible β€” so like, with -t2 and -r4, each thread gets 2 connections per period. when the rate is 0, the program just does its normal thing and creates connections at whatever variable rate it feels like. default for this option is 0. 🚨 -r and -D are mutually exclusive β€” pick a lane.
  • --rate-period=<DURATION> β€” the time period between creating connections. has to be a positive number representing how long the period is. this option gets ignored if you're not using the rate option. default is 1s.
  • -D, --duration=<DURATION> β€” the main duration for the measurements, for timing-based benchmarking. 🚨 -D and -r are mutually exclusive.
  • --warm-up-time=<DURATION> β€” the time period before the actual measurements start, for timing-based benchmarking. you have to provide this along with the -D option.
  • -T, --connection-active-timeout=<DURATION> β€” the maximum time h2load is willing to keep a connection open, no matter what's going on with said connection. <DURATION> must be a positive integer specifying how long to wait. when no timeout value is set (active or inactive), h2load will just keep a connection open indefinitely, waiting for a response.
  • -N, --connection-inactivity-timeout=<DURATION> β€” how long h2load is willing to wait to see activity on a given connection. <DURATION> must be a positive integer specifying how long to wait. same deal as above: with no timeout value set (active or inactive), h2load keeps the connection open indefinitely, waiting for a response.
  • --timing-script-file=<PATH> β€” path to a file with one or more lines separated by EOLs. every script line is made of two tab-separated fields. first field = the time offset from the start of execution, written as a positive value of milliseconds with microsecond resolution. second field = the URI. this option disables getting URIs from the command line. if <PATH> is '-', script lines get read from stdin. script lines are used in order for each client. if -n is given, it must be less than or equal to the number of script lines, and any larger values get clamped to the number of script lines. if -n isn't given, the number of requests defaults to the number of script lines. the scheme, host and port from the first URI are the only ones used β€” values in other URIs, if they're even there, get ignored. defining a base URI overrides all scheme, host or port values. 🚨 --timing-script-file and --rps are mutually exclusive.
  • -B, --base-uri=(<URI>|unix:<PATH>) β€” specify a URI whose scheme, host and port get used for all requests. the base URI overrides every value defined either at the command line or inside input files. if the argument starts with "unix:", then whatever follows gets treated as a UNIX domain socket path β€” the connection is made through that path instead of TCP. in that case, the scheme is inferred from the first URI that appears in the command line or inside the input files, like usual.
  • --alpn-list=<LIST> β€” comma delimited list of ALPN protocol identifiers, sorted by preference β€” meaning the most desirable protocol comes first. the parameter has to be delimited by a single comma only, and any white spaces are treated as part of the protocol string. careful with the vibes on that one.

Default: h2,http/1.1

  • --h1 β€” short hand for --alpn-list=http/1.1 --no-tls-proto=http/1.1, which effectively forces http/1.1 for both http and https URIs.
  • --h3 β€” short hand for --alpn-list=h3, which effectively forces HTTP/3. slay.
  • --header-table-size=<SIZE> β€” specify the decoder header table size.

Default: 4K

  • --encoder-header-table-size=<SIZE> β€” specify the encoder header table size. the decoder (that's the server) specifies the maximum dynamic table size it accepts. then the negotiated dynamic table size is the minimum of this option's value and the value the server specified.

Default: 4K

  • --log-file=<PATH> β€” write per-request info to a file as tab-separated columns: start time as microseconds since epoch; HTTP status code; microseconds until end of response. more columns might get added later, so don't get too attached. rows are ordered by end-of-response time when you're using one worker thread, but they may show up slightly out of order with multiple threads because of buffering. status code is -1 for failed streams.
  • --qlog-file-base=<PATH> β€” enable qlog output and set the base file name for qlogs. qlog gets emitted for each connection. for a given base name "base", each output file name becomes "base.M.N.sqlog" where M is worker ID and N is client ID (e.g. "base.0.3.sqlog"). only effective in QUIC runs.
  • --connect-to=<HOST>[:<PORT>] β€” host and port to connect to instead of using the authority in <URI>.
  • --rps=<N> β€” specify requests per second for each client. 🚨 --rps and --timing-script-file are mutually exclusive.
  • --groups=<GROUPS> β€” specify the supported groups.

Default: X25519:P-256:P-384:P-521

  • --no-udp-gso β€” disable UDP GSO.
  • --max-udp-payload-size=<SIZE> β€” specify the maximum outgoing UDP datagram payload size.
  • --ktls β€” enable ktls.
  • --sni=<DNSNAME> β€” send <DNSNAME> in TLS SNI, overriding the host name specified in the URI.
  • --histogram β€” plot a histogram for the performance statistics. πŸ“Š
  • --tls-session-file=<PATH> β€” read the TLS session from <PATH> and set it on all TLS connections to do session resumption. it's also used to store the new TLS session. at most one session gets written to the given file.
  • --output-file=<PATH> β€” write the measurement results to <PATH> in JSON format. this basically includes all the numbers reported to the normal output. plus, for performance measurements, all the raw samples are included too.
  • -v, --verbose β€” output debug information. (the "tell me everything" button.)
  • --version β€” display version information and exit.
  • -h, --help β€” display this help and exit.

quick little legend for the placeholders, bestie:

the <SIZE> argument is an integer plus an optional unit (so like, 10K is 10 * 1024). the units are K, M and G β€” those are powers of 1024.

the <DURATION> argument is an integer plus an optional unit (so 1s is 1 second and 500ms is 500 milliseconds). units are h, m, s or ms β€” hours, minutes, seconds and milliseconds respectively. if you leave the unit off, a second is used as the unit.

✨ OUTPUT ✨

okay bestie listen UP because this is the part where h2load actually spills the tea about what it did πŸ’… everything below is literally the report it hands you back.


πŸ“ REQUEST METRICS πŸ“

requests

your whole request-lifecycle gossip, broken down:

  • total β€” the total number of requests h2load was told to make. the assignment, basically.
  • started β€” how many requests the tool actually kicked off. no cap.
  • done β€” how many requests made it all the way to completion. we love follow-through πŸ’–
  • succeeded β€” requests that came back with an HTTP 2xx or 3xx status code. slay.
  • failed β€” the total number of failed requests. and bestie, this is a big tent: it counts both errored requests and requests that finished but with a non-2xx/3xx status code. 😭
  • errored β€” a subset of failed where the request died from network-level drama (think TCP resets, RST_STREAM) rather than from an HTTP status code. it's giving infrastructure problems fr fr.
  • timeout β€” a subset of errored where the connection just gave up and timed out before anything finished. πŸ’€

status codes

the exact count of every HTTP status code it received, sorted into classes (2xx, 3xx, 4xx, 5xx). a little status-code census, iconic.


🌈 TRAFFIC METRICS 🌈

traffic

what actually crossed the wire:

  • total β€” total application data bytes received "on the wire" (decrypted if you're using TLS, because we're nosy like that πŸ”).
  • headers β€” total bytes used for response headers, counted pre-decompression.
    • space savings β€” how efficient the header compression was, calculated as:
(1 - headers / decompressed_headers) * 100

where headers is the compressed size and decompressed_headers is the size after decompression. ✨

  • data β€” total bytes received in response bodies. the actual content, the main character.

🌸 PERFORMANCE STATISTICS 🌸

Metric Definitions

the glossary, bestie, so you know what every number is actually saying:

  • request β€” the duration from sending the first byte of a request all the way to receiving the last byte of the response. full journey, start to finish.
  • connect β€” how long it took to establish a connection, TLS handshakes included. 🀝
  • TTFB β€” the duration until the first byte of application data shows up from the server (decrypted if using TLS). time-to-first-byte, the og suspense metric.
  • req/s β€” requests per second, measured individually across all clients.
  • min RTT β€” the minimum RTT (QUIC).
  • smoothed RTT β€” the smoothed RTT (QUIC).
  • packets sent β€” the number of packets sent (QUIC).
  • packets recv β€” the number of packets received (QUIC).
  • packets lost β€” the number of packets declared lost (QUIC). and honestly, gone but not forgotten 🫢
  • GRO packets β€” the number of packets received in a single recvmsg call (QUIC).

Distribution Fields

how each of those numbers gets sliced and diced:

  • min / max β€” the absolute minimum and maximum values recorded. the full range, no filter.
  • median β€” the 50th percentile value. the middle child, but make it stats.
  • p95 / p99 β€” the 95th and 99th percentiles, which is exactly where the tail performance lives. this is the one you actually stare at. πŸ‘€
  • mean β€” the arithmetic average of all samples.
  • sd β€” the standard deviation, aka the measure of data dispersion. how spread out everything is.
  • +/- sd β€” the percentage of successful samples that fall within one standard deviation of the mean (mean +/- sd). cute little confidence zone πŸ’œ

πŸŽ€ FLOW CONTROL πŸŽ€

so here's the thing: h2load sets a large flow control window by default, and it effectively disables flow control so it doesn't under-utilize the server's performance. basically it's not here to be polite, it's here to go fast πŸ”₯

if you want a smaller flow control window, use the -w and -W options. for example, use -w16 -W16 to set the default window size described in the HTTP/2 protocol specification.

lowkey important: the defaults are deliberately huge, so if you're trying to reproduce spec-typical behaviour you gotta dial it down yourself with -w and -W.

🌟 SEE ALSO 🌟

nghttp(1), nghttpd(1), nghttpx(1) β€” the rest of the friend group, go say hi 🫢


πŸ‘‘ Author

Tatsuhiro Tsujikawa

2012, 2015, 2016, Tatsuhiro Tsujikawa


and that's a wrap bestie!! this was version 1.70.0, dated Jul 29, 2026, so go forth and benchmark responsibly πŸ’…βœ¨ bye ily, drink water πŸ“

πŸ“¦ where this page comes from