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
-cworkers waits for a response before sending its next request, so-csets concurrency, not a request rate. -qis a rate limit per worker.-q 5 -c 10sent 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/htmlunless 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.
| System | Command | Notes (November 2026) |
|---|---|---|
| macOS | brew install hey | 0.1.5 bottle, Apple Silicon and Intel |
| Any system with Go | go install github.com/rakyll/hey@latest | builds the latest tagged version |
| Linux, Windows | download from the README’s binary links | amd64 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 isresp 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:
| Run | What hey reported | Exit code |
|---|---|---|
| 200 requests to an endpoint that fails 1 in 10 | [200] 180 responses, [500] 20 responses, no error section | 0 |
20 requests to a 3-second endpoint with -t 1 | 20 errors, “Client.Timeout exceeded while awaiting headers”, Average NaN, Requests/sec 4.99 | 0 |
| 10 requests to a port with nothing listening | 10 errors, “connect: connection refused”, Requests/sec 5241.3 | 0 |
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:
-c | Requests/sec | Median | 99th percentile |
|---|---|---|---|
| 1 | 33 | 29 ms | 49 ms |
| 4 | 109 | 33 ms | 84 ms |
| 8 | 117 | 64 ms | 120 ms |
| 16 | 134 | 116 ms | 166 ms |
| 32 | 128 | 242 ms | 328 ms |
| 64 | 128 | 497 ms | 560 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.
| Tool | Load model | Strength | Watch out for |
|---|---|---|---|
| hey | Closed, -c workers | Simple flags, readable report, CSV per request | Exit 0 on failure, -q per worker |
| ApacheBench (ab) | Closed, -c connections | Preinstalled on macOS and many Linux servers | HTTP/1.0, exit 0 on 4xx and 5xx |
| wrk | Closed, -c connections over threads | Highest request rates, Lua scripting | Drops responses slower than --timeout from latency |
| Vegeta | Open, constant -rate | Fixed arrival rate, latency without coordinated omission | No 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.goandrequester/print.goat 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.