Skip to main content

hey Load Testing: A Tutorial With Real Output

hey load testing tutorial: install hey, read its report, send POSTs with tokens, and avoid the exit code 0 that hides failed runs. Tested on hey 0.1.5.

hey Load Testing: A Tutorial With Real Output

hey is a small command-line HTTP load generator written in Go. It sends a fixed number of requests with a fixed number of concurrent workers and prints a summary, a latency histogram and percentiles. A first load test is one line: hey -n 200 -c 10 http://127.0.0.1:8099/api/items, which sends 200 requests from 10 workers.

This hey load testing tutorial runs hey 0.1.5 against a small local API and shows what it printed. Four results are worth knowing before you trust a hey run: it exited with code 0 when every request failed, it counted refused connections in its requests per second (5,241 a second for 10 failures), its -q rate limit applies to each worker rather than the whole test, and it quietly dropped 5 requests when -n was not a multiple of -c.

Key Takeaways

  • hey is a closed-loop tool: each of the -c workers waits for a response before sending its next request, so -c sets concurrency, not a request rate.
  • -q is a rate limit per worker. -q 5 -c 10 sent 49 requests a second, not 5.
  • hey exits with code 0 whenever it starts, even when every request timed out or was refused. Read the “Error distribution” and “Status code distribution” sections, or parse -o csv, before calling a run good.
  • Requests per second counts errors too. Latency figures do not, so a run full of timeouts shows a fast or empty latency table.
  • hey sends Content-Type: text/html unless you pass -T, and it skips TLS certificate checks.

What is hey?

hey is an open-source HTTP load generator by Jaana Dogan (rakyll), released under the Apache 2.0 licence. Its README describes it as “a tiny program that sends some load to a web application”, and its Homebrew entry calls it an ApacheBench (ab) replacement. It was first called boom and was renamed to avoid a clash with another tool of that name.

hey speaks HTTP/1.1 by default and HTTP/2 with -h2. The current release is 0.1.5, published on 10 January 2026, more than five years after 0.1.4 (August 2020). The new release upgraded dependencies, fixed the DNS timing line, added a -U option to set the User-Agent and changed how the percentile lines print; the other options work as older guides describe. We read the 0.1.5 source for every behaviour described below and confirmed each one with a run.

How do you install hey?

On macOS, install it with Homebrew, which ships 0.1.5 as a prebuilt bottle. With Go installed, any system can build it from source. The README also links prebuilt amd64 binaries for Linux, macOS and Windows, but the 0.1.5 release on GitHub has no binaries attached.

SystemCommandNotes (November 2026)
macOSbrew install hey0.1.5 bottle, Apple Silicon and Intel
Any system with Gogo install github.com/rakyll/hey@latestbuilds the latest tagged version
Linux, Windowsdownload from the README’s binary linksamd64 only

Check the install by running hey with no arguments: it prints its usage text and the options. There is no -version flag, and unless you set -U, even 0.1.5 sends User-Agent: hey/0.0.1 on every request, so that header does not tell you which build hit your server.

How do you run your first hey load test?

Give hey a request count with -n, a worker count with -c and a full URL. Without options it sends 200 requests from 50 workers. -n cannot be smaller than -c: hey -n 5 -c 10 prints “-n cannot be less than -c.” and exits with code 1, the only failure exit we saw.

hey -n 200 -c 10 http://127.0.0.1:8099/api/items

Our target was a small Node.js API on the same laptop. Its endpoints share 4 workers that each spend 25 ms per request, so it serves at most about 160 requests a second and makes queuing visible. hey printed:

Summary:
  Total:	1.4386 secs
  Slowest:	0.1052 secs
  Fastest:	0.0319 secs
  Average:	0.0690 secs
  Requests/sec:	139.0252

Response time histogram:
  0.032 [1]	|■
  0.039 [3]	|■■
  0.047 [1]	|■
  0.054 [26]	|■■■■■■■■■■■■■■■■■
  0.061 [60]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.069 [10]	|■■■■■■■
  0.076 [7]	|■■■■■
  0.083 [54]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.091 [31]	|■■■■■■■■■■■■■■■■■■■■■
  0.098 [3]	|■■
  0.105 [4]	|■■■

Latency distribution:
  10%% in 0.0530 secs
  25%% in 0.0550 secs
  50%% in 0.0682 secs
  75%% in 0.0823 secs
  90%% in 0.0869 secs
  95%% in 0.0890 secs
  99%% in 0.1049 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0002 secs, 0.0000 secs, 0.0040 secs
  DNS-lookup:	0.0000 secs, 0.0000 secs, 0.0000 secs
  req write:	0.0000 secs, 0.0000 secs, 0.0015 secs
  resp wait:	0.0686 secs, 0.0265 secs, 0.1052 secs
  resp read:	0.0000 secs, 0.0000 secs, 0.0003 secs

Status code distribution:
  [200]	200 responses

The doubled percent signs are not a copy error. hey 0.1.5 prints %% in its latency lines because it no longer passes the report through printf, so the %% written for printf comes out as it stands; 0.1.4 printed a single %. Read 10%% in 0.0530 secs as “10% of requests finished within 53 ms”.

How do you read hey output?

Read the status code and error sections first, then the percentiles, and the average last. The summary’s Requests/sec divides every finished request, including failures, by the run time. The latency lines come from requests that got a response only. A healthy-looking latency table can therefore sit on top of a run that mostly failed.

  • Summary gives the total time, the slowest, fastest and average response, and Requests/sec.
  • Response time histogram splits the range between fastest and slowest into 10 equal steps and prints 11 rows, one per step mark; the first row holds only the fastest responses. Ours had two humps, around 60 ms and 85 ms, which is what queuing behind 4 server workers looks like with 10 requests in flight.
  • Latency distribution lists the 10th to 99th percentiles. The 99th percentile (105 ms) was about 1.5 times the median (68 ms).
  • Details splits each request into connection setup (DNS and dial), writing the request, waiting for the first byte (resp wait) and reading the body. On a slow API nearly all the time is resp wait, which is server time plus queueing.
  • Status code distribution counts HTTP responses by code. A 500 is a response, not an error, so it is counted here.
  • Error distribution appears only when a request got no HTTP response: a timeout, a refused connection or a reset.

Does hey exit with an error when requests fail?

No. In every run we tried, hey exited with code 0 once it had started, whatever happened to the requests. Only argument errors, such as -n smaller than -c, produced exit code 1. A CI step that runs hey and checks only the exit code will pass a run where the server answered nothing but errors.

We tried three kinds of failure:

RunWhat hey reportedExit code
200 requests to an endpoint that fails 1 in 10[200] 180 responses, [500] 20 responses, no error section0
20 requests to a 3-second endpoint with -t 120 errors, “Client.Timeout exceeded while awaiting headers”, Average NaN, Requests/sec 4.990
10 requests to a port with nothing listening10 errors, “connect: connection refused”, Requests/sec 5241.30

The last row shows why Requests/sec on its own means little. hey’s report code computes it as all results, errors included, divided by total time, and 10 instant refusals in 0.0019 seconds make 5,241 “requests” a second. The timeout run is the mirror image: no latency at all, because failed requests are left out of every latency figure.

To gate a pipeline on hey, write CSV and check it yourself. -o csv prints one line per request that got an HTTP response, 4xx and 5xx included, with the columns response-time, DNS+dialup, DNS, Request-write, Response-delay, Response-read, status-code, offset; requests that ended in an error do not appear. Compare the line count with the number of requests you asked for, and count the status codes:

hey -n 2000 -c 20 -o csv http://127.0.0.1:8099/api/items > run.csv
ok=$(awk -F, 'NR>1 && $7<400' run.csv | wc -l)
test "$ok" -ge 1980 || { echo "only $ok of 2000 requests succeeded"; exit 1; }

Why did hey send fewer requests than -n?

hey gives every worker -n divided by -c requests, using whole-number division, and drops the remainder. With -n 205 -c 10, each of the 10 workers sent 20 requests and the status line read [200] 200 responses, so 5 requests were never sent. Keep -n a multiple of -c when the exact count matters.

How do you set a request rate with hey?

Use -q, but multiply it by the worker count first. -q is a limit in queries per second for each worker, not for the test. Each worker waits on its own ticker before sending, so the total rate is roughly -q times -c, and lower when responses are slower than the ticker interval.

hey -q 5 -c 10 -z 5s http://127.0.0.1:8099/api/items

This sent 250 requests in 5.09 seconds, 49.1 a second: 10 workers at 5 each. -z sets a duration and overrides -n. When the time is up hey stops the workers and prints the report.

A hey rate is still a closed loop: a worker that is waiting for a slow response sends nothing, so when the server slows down, hey sends less. For a true constant arrival rate, use an open-model tool such as Vegeta, which keeps sending on schedule whether or not responses come back.

How do you send POST requests, headers and tokens with hey?

Set the method with -m, the body with -d (or -D for a file), the content type with -T and any other header with a repeated -H. Pass -T application/json (or a Content-Type header with -H) for JSON: without either, hey labels every body as text/html. We captured the raw request hey sent with only -m POST -d:

POST /api/orders HTTP/1.1
Host: 127.0.0.1:8098
User-Agent: hey/0.0.1
Content-Length: 12
Content-Type: text/html
Accept-Encoding: gzip

{"sku":"A1"}

hey has no way to log in and reuse a value from one response in the next request. Fetch the token first in the shell, then pass it as a header:

TOKEN=$(curl -s -X POST http://127.0.0.1:8099/api/login | jq -r .token)
hey -n 100 -c 5 -m POST -T application/json -d '{"sku":"A1"}' \
  -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8099/api/orders

All 100 requests returned [201]. A 20-request run without the header returned [401] 20 responses, with exit code 0 again. Every worker shares that one token, so this tests the order endpoint, not a crowd of separate users signing in.

What does hey’s -c actually measure?

-c is the number of requests in flight. Each worker sends one request, waits for the response, then sends the next. Raising -c past what the server can serve does not add throughput; it adds waiting. We ran 10 seconds at each worker count against the API that tops out near 160 requests a second:

-cRequests/secMedian99th percentile
13329 ms49 ms
410933 ms84 ms
811764 ms120 ms
16134116 ms166 ms
32128242 ms328 ms
64128497 ms560 ms

Throughput flattened at about 130 a second from 16 workers on, while from 8 workers up every doubling of -c roughly doubled the median. The rows follow Little’s Law: requests per second times average response time is about the number of workers. That is how a closed-loop tool finds a capacity limit, and also why its latency at high -c describes your test setup as much as your server. The same reasoning applies to concurrent users in a load test.

Should you disable keep-alive in hey?

Leave keep-alive on unless you are testing connection setup itself. hey reuses connections by default, like most HTTP clients. With -disable-keepalive, every request opens a new TCP connection. At 16 workers that cut throughput from 134 to 120 requests a second and raised the 99th percentile from 166 ms to 229 ms, on a local connection with no TLS.

Over the internet, and with HTTPS, a new connection per request costs far more. Turn keep-alive off to measure a TLS terminator or a load balancer’s connection handling, not to measure an API.

What else should you know before trusting hey?

Three details from the source change how you read results. hey skips TLS certificate verification (InsecureSkipVerify is set), so a test against an expired or self-signed certificate passes where browsers would fail. It keeps detailed statistics for the first 1,000,000 responses only. And -z runs for the full duration even if the server stops answering; the errors land in the error section.

hey also has no ramp-up, no think time, no assertions on response content and no way to chain requests. It answers one question: how does this one endpoint behave with this many concurrent requests from this machine.

hey vs ab, wrk and Vegeta

All four run from one machine’s command line and suit a test of one endpoint; Vegeta can also cycle through a targets file. They differ in how they generate load and in how honest their defaults are about failures. Pick by what you need to know, and run your number past a second tool when a decision rests on it.

ToolLoad modelStrengthWatch out for
heyClosed, -c workersSimple flags, readable report, CSV per requestExit 0 on failure, -q per worker
ApacheBench (ab)Closed, -c connectionsPreinstalled on macOS and many Linux serversHTTP/1.0, exit 0 on 4xx and 5xx
wrkClosed, -c connections over threadsHighest request rates, Lua scriptingDrops responses slower than --timeout from latency
VegetaOpen, constant -rateFixed arrival rate, latency without coordinated omissionNo ramp-up in the CLI

When is hey not enough?

hey is good for a quick check of one endpoint after a change, or for comparing two builds on the same machine. It cannot follow a user journey, ramp load over time, send traffic from several places or keep a history of runs.

For journeys with logins, extracted values and think time, a scripted tool such as JMeter or k6 fits better; our k6 vs JMeter vs Gatling vs Locust comparison covers how they differ. LoadFocus runs load tests from 25+ cloud regions, built in the browser or uploaded as JMeter or k6 scripts, with results shown while the test runs. For load that comes from more than one laptop, see cloud load testing.

Frequently Asked Questions

What is hey used for?

hey is used for quick HTTP load tests of a single URL: it sends a set number of requests, or runs for a set time, with a fixed number of concurrent workers, then prints a latency histogram, percentiles, a timing breakdown and status code counts. It is a modern replacement for ApacheBench.

Is hey still maintained?

Yes, but slowly. Version 0.1.5 was released on 10 January 2026, more than five years after 0.1.4. It updated dependencies, fixed the DNS timing output and added a -U option to set the User-Agent; the other options work as before.

How do I make hey send a fixed number of requests per second?

Use the -q option, which sets queries per second for each worker, and divide your target by the number of workers. For 100 requests a second with 10 workers, use -q 10 -c 10. hey still waits for each response, so a slow server gets fewer requests than the target.

Why does hey show NaN as the average?

hey prints NaN when no request got an HTTP response: it calculates the average from completed round trips only, and there were none. Look at the Error distribution section to see why, for example “Client.Timeout exceeded” when every response took longer than the -t timeout.

Does hey support HTTP/2?

Yes. Add the -h2 option and hey configures its HTTP client for HTTP/2. Without it, hey uses HTTP/1.1 with keep-alive. HTTP/2 needs a server that offers it over TLS.

Sources

  • Options, defaults and install methods: github.com/rakyll/hey, README at v0.1.5, November 2026.
  • Releases (0.1.4 on 6 August 2020, 0.1.5 on 10 January 2026) and the 0.1.5 change list: github.com/rakyll/hey, releases, November 2026.
  • Worker split, per-worker rate limit, Requests/sec formula, error handling, the 1,000,000-result cap, TLS settings and the report template: github.com/rakyll/hey, hey.go, requester/requester.go, requester/report.go and requester/print.go at v0.1.5, November 2026.
  • Homebrew package: formulae.brew.sh (hey, stable 0.1.5), November 2026.
  • Test runs: hey 0.1.5 (Homebrew) against a local Node.js 23.11.1 test API on 127.0.0.1, on an Apple M1 Max with macOS 15.7, 1 November 2026. Timings show patterns, not benchmarks.

Bogdan

Founder at LoadFocus

Bogdan builds and runs the tools this blog is about. He writes from what the products actually do in production, including the parts that break.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.