gmeter

An agent-friendly load tester for HTTP and gRPC

Gmeter sends requests on a fixed schedule and times each one from the moment it was due, so a stalled server cannot hide its slowest requests. Run it from the command line, from a Go test, or as an MCP server that an AI agent can use within limits you set.

brew install optimm/tap/gmeter
Or with Go 1.26 or newer: go install github.com/optimm/Gmeter/cmd/gmeter@latest. Windows zips and a Docker image (ghcr.io/optimm/gmeter) are on the releases page.

897µs or 505.2ms
Same run, different clock

Both numbers are the p90 of the same test: one timed from when each request was sent, the other from when it was due. Most load testers only show you the first. Gmeter keeps sending on schedule through a stall and times every request from the moment it was due, so the stall lands in your p99 instead of vanishing.

From when each request was sent what a closed-loop tester reports
p90 897µs
From when each request was due what Gmeter reports
p90 505.2ms

A server that answers instantly, except for a one-second freeze two seconds in, tested at 200 requests a second for 5 seconds. Each dot is a request, placed at the time it was sent or due, and as high as its latency. The p90s are from the real run in the README; the dots are thinned to 30 a second so you can see them.

The requests a stall delays are the ones most testers never send

Most load testers send a request, wait for the reply, then send the next. When the server freezes, they freeze with it. The requests real users would have sent during that second are never sent, so they are never measured, and the percentiles come out looking healthy. This is called coordinated omission.

Gmeter schedules request n at start + n / rate and never waits for the server to decide. A request that goes out late, because the server or Gmeter itself was busy, is still timed from when it was due. Both numbers are in every report, side by side, so you can see how much of the latency came from the stall.

service p90, from send
897µs
corrected p90, from due time
505.2ms
requests due during the freeze
200 of 1,000
corrected p99 with 20 or 2 workers
952ms, 957ms

Nine ways in, one honest clock

Run at a rate, a step test or a ramp

Constant rate, -stages for steps and ramps, Poisson arrivals with a seed you can replay. The report has corrected and service latency, a per-second timeline that puts a time on every spike, a breakdown of where each request's time went, and a line where Gmeter checks its own timing. For a server that asks the client for a certificate, give -cert and -key.

$ gmeter -rate 200 -duration 5s -workers 20 http://127.0.0.1:18080/
requests   1000 scheduled   1000 ok   0 bad status   0 failed   0 dropped
achieved   200.1 rps (target 200) over 5.00s
generator  timer lag p99 1.1ms, max 14.3ms   late 163 (16.3%)

latency, corrected (from each request's intended send time)
  p50 572µs   p90 505.2ms   p99 951.6ms   p99.9 996.7ms   max 1.00s
latency, service (from when the request was actually sent)
  p50 518µs   p90 897µs   p99 951.6ms   p99.9 996.7ms   max 1.00s

timeline (1s windows)
  p99  ▁▁█▁▁   worst 992.0ms in the window ending at 3.0s
  rps  ▆▄█▆   lowest 114 in the window ending at 2.0s

Reading the report, where the time goes and load profiles in the README.

How it compares

There are excellent load testers already. Gmeter is for when the question is "what latency will my users actually see at this load", and you want an answer you don't have to second-guess.

FeatureGmeterwrkwrk2Vegetak6Locust
Sends on a fixed schedule, whatever the server does YesNoYesYesWith the arrival-rate executorsNo
Measures latency from when each request was due YesNoYesFrom when sent; adds workers to stay on timeFrom when sent; iterations it can't start are dropped and countedNo
Checks its own timing and says when it is the bottleneck Timer lag and late sends, with warningsNoNoNoCounts dropped iterationsWarns on high CPU
Searches for the highest rate that meets a latency target YesNoNoNoNoNo
Replays an access log at its recorded pace Yes, per endpointNoNoIts requests, not their timingBy scripting itBy scripting it
Splits each request into dns, connect, tls, send, wait, transfer Yes, with Gmeter's own queueing tooNoNoNoYesNo
Compares two runs and gates a build on the difference Yes, from full histogramsNoNoNoPass or fail thresholds per runNo
HTTP/2 Yes, TLS and h2cNoNoYesYesNot built in
gRPC UnaryNoNoNoYes, streaming tooWith a custom client
Several machines, one merged result Yes, histograms addedNoNoMerge the result files yourselfk6 Operator or Grafana CloudYes, built in
Importable as a Go library Yes, with an SLO checkNoNoYesExtensions are Go; tests are JavaScriptPython, not Go
Scripted user journeys NoLuaLuaNoJavaScriptPython

Based on each project's documentation and source as of November 2026. If a cell is out of date, open an issue and it will be fixed.

When to use something else

  • Multi-step user journeys with logins, state and think time: k6 or Locust.
  • Streaming gRPC methods: k6 or ghz.
  • A browser in the loop: k6's browser module or Playwright.
  • A hosted dashboard for a team to share: Grafana Cloud k6 or Locust's web UI.

Under the hood

Never averages percentiles
Every result keeps its full log-linear histogram: about 250KB, with at most 0.1% error at any size. Stages, windows and agents are combined by adding histograms together.
No drift over millions of requests
Request n's due time is computed from n, never by adding up intervals, so rounding errors can't accumulate. A generator that falls behind catches up, and each late request keeps its due time.
Precise timing, at a small CPU cost
The scheduler sleeps, then busy-waits the last 200µs. At 100 rps that brought the corrected p50 from 2.4ms down to 601µs, against a service p50 of 541µs. The busy-wait is capped at a quarter of a core and turns itself off above about 5,000 rps.
Recording without locks
Each worker records into its own atomic histogram, padded to a 128-byte cache line, and a sampler reads them. With 8 goroutines recording at once, padding took a record from 2.0–3.1ns down to 0.92ns.
Three dependencies, all pinned
Everything is Go's standard library except gRPC support, which pins grpc v1.85.0 and protobuf v1.36.12, and the MCP server, which pins the official Go SDK v1.8.0.
Safe by default
Replays send only GET and HEAD unless you say otherwise. Agents listen on localhost unless told otherwise, and read their password from an environment variable, never a flag. Passwords in URLs are redacted everywhere they're printed or saved. The MCP server targets only loopback until you allow more, and caps the load an agent can ask for.

Point it at something

$ brew install optimm/tap/gmeter
$ gmeter -rate 500 -duration 30s http://localhost:8080/health
$ gmeter -stages '30s@200,30s@400,30s@800' -arrivals poisson http://localhost:8080/
$ gmeter find -slo p99=50ms http://localhost:8080/health

Every flag and every caveat is in the README, with real output from real runs.