You want to know how your API behaves at exactly 500 requests per second, not “as fast as N threads can hammer it.” Most load tools fix concurrency and let the request rate float. Vegeta does the opposite: you set the rate, and it sends that many requests per second regardless of how the server responds. That difference matters when you care about the number the answer sheet uses, throughput at a target load, and the latency you’d actually promise in an SLO.
What Vegeta is
Vegeta is a command-line HTTP load testing tool written in Go. It’s also usable as a Go library, but this tutorial sticks to the CLI. The core idea is a constant request rate. You tell Vegeta “send 100 requests per second for 30 seconds,” and it holds that pace by adding workers as needed. If the server slows down, Vegeta keeps issuing new requests on schedule instead of waiting for old ones to finish.
That design gives you an open-model load test. Real traffic arrives on its own clock; users don’t pause and wait for your slow endpoint before the next user shows up. A rate-based tool matches that behavior.
Rate-based vs concurrency-based load
Here’s the split that trips people up.
A concurrency-based tool (a fixed pool of threads or virtual users, each looping request-response) uses a closed model. When the server gets slow, the loop stalls, so the actual request rate drops. You asked for load, but the tool quietly backed off. That hides the pileup you’d see in production.
A rate-based tool like Vegeta uses an open model. You fix the arrival rate. If the server can’t keep up, requests queue, latency climbs, and the report shows it. You get an honest picture of what happens when demand exceeds capacity.
Neither model is wrong. Use concurrency when you want to know “how many simultaneous users can I hold.” Use a rate model when you want to know “what happens at 2,000 requests per second.” Vegeta is the second kind. For a broader map of the tooling space, see the top API load testing tools.
Install Vegeta
Pick whichever fits your setup.
Homebrew on macOS:
brew update && brew install vegeta
Other package managers:
# MacPorts
port install vegeta
# Arch Linux
pacman -S vegeta
# FreeBSD
pkg install vegeta
From source with Go installed:
git clone https://github.com/tsenart/vegeta
cd vegeta
make vegeta
You can also download a pre-built binary from the GitHub releases page. Confirm the install:
vegeta --help
The core pipeline
Vegeta is built around Unix pipes. A target goes in, an attack runs, and a report comes out. The smallest useful run is one line:
echo "GET http://localhost:8080/" | vegeta attack -duration=5s -rate=100 | vegeta report
Read that left to right:
echowrites one target (a method and URL) to standard output.vegeta attackreads that target from standard input and fires 100 requests per second for 5 seconds. That’s 500 requests total.vegeta reportreads the binary results stream and prints a summary.
The -rate flag takes requests per time unit. -rate=100 and -rate=100/1s mean the same thing. -rate=50/500ms means 50 requests every 500 milliseconds. Setting -rate=0 removes the limit and sends as fast as possible, which is a different test entirely.
A text report looks like this:
Requests [total, rate, throughput] 500, 100.20, 100.18
Duration [total, attack, wait] 4.991s, 4.990s, 1.2ms
Latencies [min, mean, 50, 90, 95, 99, max] 412us, 1.3ms, 1.1ms, 1.9ms, 2.4ms, 5.1ms, 12ms
Bytes In [total, mean] 128500, 257.00
Bytes Out [total, mean] 0, 0.00
Success [ratio] 100.00%
Status Codes [code:count] 200:500
Error Set:
Read the latency line first. The 50 column is the median. The 99 column is the tail: 1 percent of requests were slower than that. Tails are where users feel pain, so a healthy mean with an ugly 99th percentile is still a problem. Check Success [ratio] and Status Codes next. A 100 percent success ratio with all 200s means the server held the load. Any 5xx codes or a non-empty Error Set mean it started breaking.
Save results, then report many ways
Piping straight to report is fine for a quick look. For anything you’ll revisit, save the raw results to a file first, then generate every report you want from that one file.
echo "GET http://localhost:8080/" | \
vegeta attack -duration=10s -rate=200 -output=results.bin
Now the run is captured in results.bin. Produce a text report:
vegeta report results.bin
Produce structured JSON for a dashboard or a diff between runs:
vegeta report -type=json results.bin > metrics.json
Produce a latency histogram with buckets you choose:
vegeta report -type='hist[0,2ms,5ms,10ms,25ms,100ms]' results.bin
The histogram counts how many requests fell into each latency band. It’s a quick way to see whether latency is tightly clustered or smeared across a wide range.
The targets file format
Real APIs need more than one GET. Move your targets into a file and pass it with -targets. The default HTTP format is line-based and readable.
Create targets.txt:
GET http://localhost:8080/api/users
POST http://localhost:8080/api/users
Content-Type: application/json
@./payload.json
GET http://localhost:8080/api/users/42
A few rules make this work:
- The first line of each target is
METHOD URL. - Header lines follow directly under the method line, one
Key: Valueper line. - A line starting with
@names a file whose contents become the request body. - A blank line separates one target from the next.
- Lines starting with
#are comments.
Put your JSON body in payload.json:
{ "name": "Ada", "role": "engineer" }
Run the attack against the file:
vegeta attack -targets=targets.txt -rate=50 -duration=30s | vegeta report
Vegeta cycles through the targets in order and keeps looping until the duration ends. You can also add global headers on the command line with -header, which is handy for auth:
vegeta attack -targets=targets.txt -rate=50 -duration=30s \
-header="Authorization: Bearer $TOKEN" | vegeta report
For programmatic or high-volume runs, Vegeta reads a JSON target format too. Each line is one JSON object, and you select it with -format=json:
{"method": "GET", "url": "http://localhost:8080/api/users"}
{"method": "POST", "url": "http://localhost:8080/api/users", "header": {"Content-Type": ["application/json"]}, "body": "eyJuYW1lIjoiQWRhIn0="}
The body field is base64-encoded. This format streams well, so you can generate targets on the fly and pipe them straight in with -lazy for memory efficiency.
Plots and histograms
A single number hides trends. If latency degrades halfway through a run, you want to see the curve. Vegeta’s plot command turns a results stream into an interactive HTML page:
vegeta attack -targets=targets.txt -rate=100 -duration=60s | \
vegeta plot > plot.html
Open plot.html in a browser. You get a time-series chart of latency across the run, so a slow ramp or a mid-test spike is obvious. You can plot several runs together to compare load levels:
vegeta attack -rate=50 -duration=30s -targets=targets.txt -output=50qps.bin
vegeta attack -rate=100 -duration=30s -targets=targets.txt -output=100qps.bin
vegeta plot 50qps.bin 100qps.bin > compare.html
For headless environments, skip the plot and export a histogram or the raw CSV with encode:
vegeta encode -to=csv results.bin > results.csv
When to use Vegeta
Reach for Vegeta when the question is about rate and capacity:
- You need throughput and latency at a specific requests-per-second target.
- You want to find the rate at which latency or error rate starts climbing.
- You’re comparing two builds or two infrastructure configs under identical, repeatable load.
- You want a scriptable tool that drops into a shell pipeline and a CI job with no GUI.
It’s a focused instrument. It sends HTTP requests at a rate and measures how the server responds. It doesn’t script multi-step user journeys with branching logic, and it doesn’t validate response bodies beyond status codes. That focus is a feature; it keeps the tool small and the results clean.
If you’re weighing it against other runners, the comparisons in k6 load testing and JMeter load testing cover the concurrency-model alternatives. For the underlying concepts and metrics, the API performance testing tutorial is a good primer.
Where functional testing fits
Load testing answers “is it fast enough.” It does not answer “is it correct.” A Vegeta run can report 100 percent 200s while the endpoint returns the wrong user, a stale price, or a malformed JSON field. Every one of those responses is a fast, successful 200 as far as a load tool is concerned.
Correctness needs a different check: assertions on the response body, the schema, and the business rules. That’s functional testing, and it belongs before and alongside your load runs. The healthy pattern is to validate that the API is right, then measure how it performs under rate.
This is where Apidog complements a tool like Vegeta rather than competing with it. In Apidog you build test scenarios with visual assertions on status, headers, and JSON fields, chain requests, and drive them from data files. You confirm the API returns the correct data. Vegeta then confirms it stays fast when that correct path is hit hundreds of times a second. Two tools, two questions.
Apidog also ships a headless CLI so those functional scenarios run in the same CI pipeline as your load step. Install it with Node:
npm install -g apidog-cli
Then run a saved scenario or suite by id, with reporters for your pipeline:
apidog run --access-token "$APIDOG_ACCESS_TOKEN" \
-t <scenarioOrSuiteId> \
-e <environmentId> \
-r cli,html,junit
The -t flag takes the scenario, folder, or suite id, -e takes the environment id, and -r picks one or more reporters (cli, html, json, junit). The junit output slots into most CI dashboards. See the Apidog CLI command-line tutorial for a step-by-step run, and the CI/CD pipeline guide for a copy-paste workflow. Run the functional gate first, then let Vegeta measure the endpoints that passed.
FAQ
Does Vegeta support POST requests with a body?
Yes. In the HTTP targets file, put the method and URL on the first line, add a Content-Type header, and reference a body file with @./payload.json. In the JSON format, set the method, url, and a base64-encoded body field.
What does -rate=0 do?
It removes the rate limit and sends requests as fast as the workers and connections allow. That’s a max-throughput test, which is different from a controlled constant-rate test. For repeatable capacity measurements, set an explicit rate.
How do I read the latency percentiles?
The report’s latency line shows min, mean, and the 50th, 90th, 95th, 99th, and max percentiles. Focus on the 95th and 99th. They describe the slow tail that real users hit, which a mean alone can hide.
Can Vegeta check that my API returns correct data?
No. It measures throughput, latency, and status codes. It does not assert on response bodies or schemas. Pair it with a functional testing tool for correctness, then use Vegeta for rate and latency.
How do I run Vegeta in CI?
It’s a single binary that reads stdin and writes results, so it drops into any shell step. Save results with -output, then generate a text or JSON report as a build artifact. Add a functional gate, such as the Apidog CLI, ahead of the load step so you only measure endpoints that already pass their assertions.



