Skip to main content

Vegeta Load Testing: A Tutorial With Real Output

Vegeta load testing step by step: install, attack at a fixed rate, write targets files, read reports, ramp up and gate CI. Tested on 12.13.0 with real output.

Vegeta Load Testing: A Tutorial With Real Output

Vegeta load testing means piping a list of HTTP requests into vegeta attack, which sends them at a fixed rate for a set time, and then reading the binary results with vegeta report. A first test is one line: echo "GET http://127.0.0.1:8080/" | vegeta attack -rate=100 -duration=10s | vegeta report. Vegeta is a free, open-source command-line tool written in Go.

This tutorial runs Vegeta 12.13.0 against a small local API and shows what it printed: a first attack, what happens when the rate is more than the server can handle, targets files with headers and bodies, a stepped ramp-up (Vegeta has none built in), and how to fail a CI job on the results.

Key Takeaways

  • Vegeta sends requests at the rate you set whether or not responses come back. Past the server’s capacity, our median went from 26 ms to 1.37 seconds, while ApacheBench at -c 32, which waits for responses, showed a 212 ms median.
  • -rate=0 -max-workers=N switches to a fixed number of workers that each wait for a response. Vegeta refuses -rate=0 without -max-workers.
  • Read Success before the latencies. With -timeout=100ms, every request failed and the latency row still showed a 100 ms median.
  • vegeta attack and vegeta report exit with code 0 even when every request failed. Gate CI on report -type=json with jq.
  • The vegeta command has no ramp-up flag (issue #418 has been open since 2019). Run one attack per step and report each step on its own, or use the Go library’s LinearPacer.

What is Vegeta?

Vegeta is an HTTP load testing tool and Go library by Tomás Senart, released under the MIT licence. Its README describes it as built “out of a need to drill HTTP services with a constant request rate”. The current release is 12.13.0, published on 31 October 2025, which moved vegeta plot to a new charting library.

The tool is a set of small commands joined by pipes: attack sends requests and writes binary results, report summarises them, plot draws latency over time as an HTML page, and encode converts results to JSON or CSV. Because results are files, you can run an attack once and report on it as many ways as you like afterwards.

How do you install Vegeta?

On macOS, install it with Homebrew (brew install vegeta) or MacPorts. On Linux, Windows and the BSDs, download the archive for your system from the project’s GitHub releases page, which also publishes a checksums file. Arch Linux and FreeBSD package it as vegeta. Check the result with vegeta -version.

We downloaded the darwin_arm64 archive for 12.13.0, checked it against vegeta_12.13.0_checksums.txt with shasum -a 256 -c, and ran the binary from a working folder. vegeta -version printed Version: v12.13.0 and Runtime: go1.22.12 darwin/arm64.

How do you run a Vegeta attack?

Pass a target on standard input, set a rate with -rate and a length with -duration, and pipe the results to vegeta report. Without -rate, Vegeta sends 50 requests a second. Without -duration, it attacks until you press Ctrl-C, and the results up to that point are still valid.

echo "GET http://127.0.0.1:18080/api/products" | \
  vegeta attack -rate=100 -duration=10s | tee results.bin | vegeta report

Our target was a small Node.js API on the same laptop. The product endpoint returns 803 bytes of JSON. tee keeps the binary results in results.bin so you can report on them again. The report:

Requests      [total, rate, throughput]         1000, 100.11, 100.10
Duration      [total, attack, wait]             9.99s, 9.99s, 767.375µs
Latencies     [min, mean, 50, 90, 95, 99, max]  168.708µs, 418.363µs, 329.688µs, 509.671µs, 747.487µs, 2.114ms, 8.425ms
Bytes In      [total, mean]                     803000, 803.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:1000
Error Set:

To check the “results are still valid” part, we started an attack with no -duration and sent it an interrupt after 3 seconds. It exited with code 0 and its file reported 151 requests over 3 seconds at 50.34 a second, the default rate.

How do you read a Vegeta report?

Read Success and Status Codes first, then the latency percentiles, then compare rate with throughput on the first line. rate is how fast Vegeta sent requests; throughput is successful responses per second over the whole run. When the two differ, requests failed or the server fell behind.

What each row means, from the README:

  • Requests: the total sent, the rate Vegeta actually held, and the throughput of successful requests.
  • Latencies: the time to read each response, as the minimum, mean, 50th, 90th, 95th and 99th percentiles and the maximum.
  • Bytes In / Bytes Out: response and request body bytes, in total and per request.
  • Success: the share of requests that did not error and got a status code from 200 up to, but not including, 400.
  • Error Set: each distinct error once, including HTTP errors such as 500 Internal Server Error.

In the Duration row, attack is the time spent sending, wait is the time spent waiting for the last responses after sending stopped, and total is both; a long wait means responses were still queued when the attack ended. In Status Codes, the README says 0 means a request “failed to be sent”; in our runs, refused connections and timeouts both showed as 0.

Latencies are only meaningful next to Success. In one run we set -timeout=100ms against an endpoint that takes 200 ms. Every request failed (Status Codes 0:40, Success 0.00%, error context deadline exceeded (Client.Timeout exceeded while awaiting headers)), but the latency row showed a 100.395 ms median, because each failed request was recorded at the timeout. A run against a stopped server showed 0:10 and connect: connection refused.

What does -rate mean in Vegeta?

-rate is how many requests Vegeta starts per time unit, such as -rate=200 or -rate=200/1s, and it does not slow down when the server does. Vegeta adds workers as needed to keep the schedule, starting from 10 (-workers). This is an open model: like real visitors, new requests keep arriving whether or not earlier ones have finished.

We pointed Vegeta at an endpoint that runs on a pool of 4 workers and does 25 ms of work per request, so in theory it can finish 160 requests a second; Vegeta measured about 155. Each row is a separate 10-second attack:

-rateThroughputMedian99th percentileMaxWait after sending
10099.8426.5 ms48.9 ms113.5 ms27.7 ms
140139.7526.0 ms28.3 ms34.7 ms26.3 ms
200155.641.373 s2.832 s2.87 s2.856 s

Below capacity, the median request took about 26 ms. At 200 a second, Vegeta sent 2,000 requests in 10 seconds, the server finished 155 a second, and the other 45 a second joined a queue that grew for the whole test. The median request waited 1.4 seconds, and the run took 12.85 seconds because the queue needed almost 3 more seconds to drain.

Overload on the same endpoint looks mild in a tool that waits for each response. ApacheBench with 32 concurrent requests against this endpoint reported a 212 ms median, because it only sent a new request when an old one finished, so the queue could never grow past 32. Vegeta’s README says it “avoids nasty Coordinated Omission”, the name for this effect, where the load tool slows down along with the server and under-reports how slow it got.

-rate=0 switches to the other model. With -max-workers=8, Vegeta ran 8 workers that each sent the next request only after the last one answered: 149.73 requests a second with a 53.5 ms median, close to what ApacheBench gave with 8 concurrent requests (147 a second, 54 ms). Without -max-workers, Vegeta refused to start and printed -rate=0 requires setting -max-workers.

How do you write a Vegeta targets file?

A targets file lists requests in a format close to raw HTTP: a method and URL on one line, headers on the lines below it, an @path line for a body file, and a blank line between requests. Lines starting with # are comments. Pass the file with -targets, and Vegeta sends its requests in turn.

# log in
POST http://127.0.0.1:18080/api/login
Content-Type: application/json
@login.json

# read the profile with a token
GET http://127.0.0.1:18080/api/me
Authorization: Bearer <your-token>

GET http://127.0.0.1:18080/api/products
vegeta attack -targets=targets.txt -rate=30 -duration=5s > t.bin
vegeta encode --to=csv t.bin | awk -F, '{print $10, $11, $2}' | sort | uniq -c

The 150 requests from that attack split exactly 50, 50 and 50 across the three targets, all HTTP 200, and Bytes Out totalled 1,800: the 36-byte login body sent 50 times. The encode line counts requests per method, URL and status, since the CSV’s 10th, 11th and 2nd columns hold them.

Without the Authorization header, a 2-second run against /api/me at 20 a second reported Success 0.00%, 401:40 and 401 Unauthorized in the error set.

This format, which Vegeta calls http, cannot hold a body inline, only a file reference, and it does not chain requests: Vegeta will not read the token from the login response and use it in the next request, so we fetched a token with curl first and pasted it in. For bodies that change per request, Vegeta also reads targets as JSON lines (-format=json, with base64 bodies), which a script or jq can generate as a stream with -lazy.

How do you ramp up load in Vegeta?

The vegeta command has no ramp-up flag: each attack runs at one rate. The GitHub issue asking for one, “Add the ability to ramp-up the load” (#418), has been open since July 2019. From the command line, the workaround is a shell loop that runs one attack per step at a rising rate, writing each step to its own file.

for r in 50 100 150 200; do
  echo "GET http://127.0.0.1:18080/api/report" | \
    vegeta attack -rate=$r -duration=10s -name=step$r > step$r.bin
done
vegeta plot -title="Stepped ramp" step50.bin step100.bin step150.bin step200.bin > ramp.html

Report each step separately. Passing all four files to one vegeta report merges them by timestamp into one summary of 5,000 requests over 43 seconds: a 27 ms median and a 2.79-second 99th percentile, which hides that the last step’s requests queued for seconds. Per step, the picture is different:

StepThroughputMedian99th percentile
50 a second50.026.7 ms30.2 ms
100 a second99.926.4 ms28.2 ms
150 a second149.726.5 ms31.4 ms
200 a second155.61,374 ms2,837 ms

The server kept up to 150 a second and fell behind at 200; the merged report hid where. vegeta plot takes several files and draws them on one timeline, which shows the jump at the last step. Our four-step plot was a 269 KB HTML file that opens in a browser. For a smooth ramp rather than steps, Vegeta’s Go library has LinearPacer, which starts at a given rate and raises it linearly with a slope you set; you pass it to Attack in a small Go program instead of a fixed rate. Outside Go, k6’s ramping-arrival-rate executor raises an arrival rate through stages and is also an open model.

How do you fail a CI job with Vegeta?

Vegeta’s exit codes do not reflect results: vegeta attack and vegeta report both exited 0 for our run where every request got HTTP 401. Use vegeta report -type=json, which prints the success ratio, status codes and latencies (in nanoseconds), and test it with jq -e, which exits with 1 when the expression is false.

vegeta report -type=json results.bin | \
  jq -e '.success == 1 and .latencies["99th"] < 500000000'

That gate requires every request to succeed and the 99th percentile to stay under 500 ms. It exited 0 for the 140-a-second run, and 1 for both the 200-a-second run (99th percentile 2.83 seconds) and the all-401 run. To see where requests fell, -type='hist[0,25ms,50ms,100ms,1s]' prints a text histogram: in the 200-a-second run, 63.80% of requests took 1 second or more.

When is one machine not enough?

Vegeta runs on one machine, and its README says that when a test hits that machine’s limits, such as open files, memory, CPU or network bandwidth, you run it on several machines, divide the rate between them, copy the result files back and report on them together. You also own each machine’s ulimit settings.

That works for a single endpoint or a fixed list of requests. Journeys where one response feeds the next request need a scripting tool, and our k6 vs JMeter vs Gatling vs Locust comparison covers four of them. LoadFocus runs load tests from 25+ cloud regions without load generators to manage: build a test in the browser or upload a JMeter or k6 script, and results show while the test runs. For a quick check of one endpoint, the free API load test runs from the browser.

Frequently Asked Questions

Is Vegeta free?

Yes. Vegeta is open source under the MIT licence, and both the command-line tool and the Go library are free to use. You install it from Homebrew, MacPorts, the Arch Linux or FreeBSD package, or a prebuilt binary from its GitHub releases page. The only cost is the machines that send the load.

What is the difference between Vegeta and ApacheBench?

Vegeta sends requests at a fixed rate whether or not the server keeps up, while ApacheBench keeps a fixed number of requests in flight and waits for each response. Past capacity, Vegeta shows the growing queue that real visitors would hit. Vegeta also reads several targets from a file, supports HTTP/2, and saves results you can report on again.

Does Vegeta support ramp-up?

Not from the command line. Each vegeta attack runs at one rate, and the request to add a ramp-up option (GitHub issue #418) has been open since 2019. Run one attack per step in a shell loop, with a rising -rate and a separate output file for each step, then report on each file separately and plot them together with vegeta plot. In Go, Vegeta’s library has a LinearPacer that raises the rate linearly.

How do I send a POST request with Vegeta?

Write a targets file with the method and URL on the first line, headers such as Content-Type: application/json below it, and @body.json pointing to the body file. Run it with vegeta attack -targets=targets.txt. For one target, you can also pass -body=body.json and -header="Content-Type: application/json" on the command line.

Why does Vegeta show status code 0?

Status code 0 means the request never received an HTTP response. Vegeta could not connect, the connection broke, or the request ran past -timeout, which is 30 seconds by default. The Error Set lists the reason, such as connection refused or Client.Timeout exceeded while awaiting headers. These requests count as failures in the Success ratio.

Sources

  • Commands, flags, targets formats, report fields, plot and distributed attacks: github.com/tsenart/vegeta, README, October 2026.
  • Release 12.13.0 (31 October 2025), its changelog and checksums file: github.com/tsenart/vegeta/releases, October 2026.
  • MIT licence: github.com/tsenart/vegeta, LICENSE, October 2026.
  • LinearPacer and Attack: github.com/tsenart/vegeta, lib/pacer.go and lib/attack.go at tag v12.13.0, October 2026.
  • Ramp-up request: github.com/tsenart/vegeta/issues/418, “Add the ability to ramp-up the load”, opened 15 July 2019, open in October 2026.
  • k6 ramping arrival rate: grafana.com/docs/k6, “Ramping arrival rate” executor, October 2026.
  • Test runs: Vegeta 12.13.0 (darwin/arm64) against a local Node.js 23.11.1 test API on 127.0.0.1, with ApacheBench 2.3 for the comparison runs, 29 October 2026. Timings are from one laptop running other work and 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.