Feature map
What surge does, and where to read more. Written to be checked against whatever you use today.
Writing a test
| surge | |
|---|---|
| Scenario in code | Go — s.User(...), u.Task(...), compiled, any Go library available |
| Scenario without code | YAML — flow, extract, check, feeders, templating |
| Generate from a browser recording | surge import har session.har — with automatic correlation |
| Generate from an API description | surge import openapi spec.yaml |
| User populations with weights | s.User("buyer", 3) |
| Exactly N of a population | u.FixedCount(2) |
| Weighted tasks | u.Task("browse", 4, fn) |
| Sequential journeys | u.Sequential() |
| Task tags, include/exclude | u.TaggedTask(...), -tags smoke -exclude-tags slow |
| Per-user setup / cleanup | u.OnStart() / u.OnStop() |
| Global setup / teardown | s.Setup() / s.Teardown() |
| Groups (nested, timed) | c.Group("checkout", fn) |
| Think time | .Wait(min,max) · Constant · Pacing · Throughput · Poisson |
| Parallel requests | c.Batch(surge.Req(...), ...) |
| Per-user cookie jar | s.CookieJar() |
| Per-request timeout | surge.Timeout(d) |
| Response validation | surge.Validate(fn) — a 200 that is wrong is a failure |
| Name from the response | surge.NameFrom(fn) |
| Structured logging, capped | c.Log(), c.Infof(), s.Logging(surge.LogDebug) |
Load models
| surge | |
|---|---|
| Fixed virtual users | -vus N · s.Closed(n, dur) |
| Arrival rate (open model) | -rate R · s.Open(r, dur) |
| Ramping stages | stages: · s.ClosedStages(...) / s.OpenStages(...) |
| Fixed iteration count | -iterations N · s.Iterations(n, vus) |
| Automatic capacity search | surge discover — climbs, bisects, verifies |
| Warmup excluded from stats | -warmup 30s |
| Graceful stop | grace_stop, default 5s |
| Arrivals that could not be applied | counted as dropped, never hidden |
Correlation and data
| surge | |
|---|---|
| Extract from JSON | extract: { token: { json: data.token } } |
| Extract from a header | { header: X-Request-Id } |
| Extract with a regex | { regex: 'name="csrf" value="([^"]+)"' } |
| CSV feeders | circular · random · once |
| Templating | ${var} ${env.X} ${__VU} ${uuid()} ${randint(a,b)} ${randstr(n)} ${now()} ${now_iso()} |
| Per-VU state | c.Set() / c.Str() / c.Value() |
Checks and gating
| surge | |
|---|---|
| Status / body / JSON / latency checks | check: block, c.Check(), surge.Expect(), surge.MaxDuration() |
| Thresholds | p50/p90/p95/p99/avg/min/max, error_rate, check_rate, rps, count, scoped per request |
| Exit code | 0 pass · 1 threshold failed · 2 setup error |
| Regression gate vs baseline | surge compare — median ±MAD, refuses to gate on too few runs |
| Trend across releases | surge trend — the slope no single comparison can see |
| Auto-stop | -abort-on "error_rate > 50% for 15s" — stop a test that has already failed |
| Custom gating | s.OnTestStop(func(*Summary)) |
Protocols
| surge | |
|---|---|
| HTTP/1.1, HTTP/2 | native, keep-alive, redirects, proxy, TLS options |
| WebSocket | in YAML: ws:/as: to dial, on:+send/receive for messages, repeat: for a stream, checks and extracts on the message. In Go: c.Dial(), Send, Receive, Request, Listen |
| gRPC | native — grpcx.Dial, grpcx.Call, grpcx.Stream, status-code classification |
| Databases | dbx — any database/sql driver, errors grouped by cause, pool queueing reported |
| SAP OData / Fiori | sapx — CSRF handshake, expiry retry, $batch with changesets |
| Mainframe TN3270 | tn3270 — telnet negotiation, EBCDIC, screen buffer, transaction timing |
| Anything else | c.Time(name, fn) — queue, TCP, an SDK, your own client |
Metrics and reports
| surge | ||||||
|---|---|---|---|---|---|---|
| Latency histogram | log-linear, ~3% error, fixed memory, mergeable across nodes | |||||
| Percentiles | p50/p90/p95/p99 by default, -percentiles 99.9,99.99 for more | |||||
| Full distribution | the ladder to p99.9 and max, as columns on the request row — one table, shown on request | |||||
| Per-second percentiles | p50 and p95 on the timeline, not just the mean — blank for a merged run, because percentiles cannot be averaged | |||||
| Custom metrics | Counter · Gauge · Trend · Rate | |||||
| Error classification | timeout · refused · reset · dns · HTTP n | |||||
| Why it passed or failed | a verdict that names the breached threshold and the actual number — console, report, summary.json, webhook | |||||
| Reasons to distrust the result | a run cut short, arrivals never applied, a saturated generator, a feeder too small, nothing asserted at all — stated, not implied | |||||
| Failure evidence | one real example per distinct failure: role, URL, status, failed checks, and what the server replied | |||||
| Bounded by design | failures grouped by signature, bodies capped at 512 bytes, so a million failures cost what fifty do | |||||
| Private by request | output.no_failure_bodies keeps the replies out | |||||
| Checks by name | json status = confirmed — 113 of 799 failed, not 97.1% | |||||
| Exceptions | a task that raised, with its message and stack, grouped | |||||
| Timing breakdown | -timings — blocked, DNS, connect, TLS, send, TTFB, receive, with the dominant phase explained | |||||
| Server-side correlation | -monitor — scrape the target's own metrics and rank what moved with the latency | |||||
| Console | live line + final table | |||||
| JSON summary | full, machine-readable | |||||
| Per-second CSV | sec,reqs,errs,avg_ms,p50_ms,p95_ms,max_ms,vus | |||||
| Per-request CSV | -log-requests — for outliers and forensics | |||||
| Failures CSV | -failures — one row per distinct failure, with its example | |||||
| HTML report | one self-contained file, light and dark with a toggle, no CDN | |||||
| Report charts | throughput with failures · p50/p95/max · virtual users | |||||
| Report tables | sortable, filterable, paged — a run that failed ten thousand ways stays readable | |||||
| Test kind | `type: smoke\ | load\ | stress\ | spike\ | soak\ | capacity` — inferred from the plan when absent, and the report says when the thresholds do not suit the kind |
| Setup / teardown | setup: and teardown: run around the load, in the scenario's directory, once on the coordinator. Teardown runs on a failed run, a breached threshold and a Ctrl-C | |||||
| Preflight | surge preflight — DNS, TCP, TLS expiry, redirects, clock skew, feeder rows, monitor endpoints, every agent's health and build, this machine's fd limit. One request each, no load, GO or NO-GO with an exit code | |||||
| User actions | group: on consecutive steps — one action timed end to end, think time excluded, in its own table and never added to the totals | |||||
| Who did the work | the mix that ran against the mix that was asked for — a weight decides how many users pick a role, not how fast they go round | |||||
| Run log | timed events you can line up against the charts: warnings, generator saturation, abort rules firing, with the node in a fleet | |||||
| Report tabs | pinned to the top: overview · over time · actions · requests · failures · checks · exceptions · agents · log · server · run · data; a failing run opens on the failures, a passing one on the overview | |||||
| Users and workers | in the header, measured not planned: the peak virtual users reached and the machines that produced them | |||||
| Per-node breakdown | each generator's users, share, iterations, its own latency and whether it was healthy while producing it | |||||
| The plan, recorded | target, ramp, warmup, shard, populations, feeders, thresholds — what the number was measured under | |||||
| Control UI | surge web — pick a scenario, override users/duration/target, Start, watch, Stop, open the report. Binds loopback; anywhere else needs -token; scenarios that run shell commands need -allow-exec | |||||
| Live web dashboard | -web :8088, read-only, attached to a running test | |||||
| Prometheus | -prometheus text exposition → Pushgateway → Grafana | |||||
| InfluxDB | -influx line protocol | |||||
| Webhook | -webhook URL posts the summary | |||||
| Automatic analysis | -analyze — knee detection, bottleneck, error meaning, trustworthiness | |||||
| Raw, mergeable form | -snapshot — histogram buckets, combined by surge merge |
Running it
| surge | |
|---|---|
| One binary, no runtime | static, cross-compiled; 35MB container |
| Headless / CI | the default; the console is the extra |
| Distributed, free | surge agent + -agents a,b,c, raw histograms merged |
| Fails fast on a bad fleet | unreachable, busy or mismatched agent stops the run before traffic |
| Ctrl-C reaches the agents | yes |
| Docker | deploy/docker — image plus compose with replica scaling |
| Kubernetes | Deployment + headless Service + Job (deploy/kubernetes) |
| Config file | surge.conf, searched upward then in $HOME |
| Environment | SURGE_* for every flag |
| Precedence | config file → environment → flags |
| Generator-bottleneck warning | 85% CPU, or goroutine scheduling delay above a tenth of the measured p95 — a throttled container reads low on CPU precisely because the kernel is holding it back |
| Watched on every worker | not just the coordinator; a fleet is made of small containers, which is where it happens |
| Validate without traffic | surge validate |
| Rehearse against the real system | surge verify, or surge run -verify — one pass of every role, every extract and check reported, before any load |
| Split with no coordinator | -shard 2/5, feeder rows divided too, then surge merge |
| A run that measured nothing | cannot pass; says NO MEASUREMENTS |
| A panicking virtual user | recorded as an exception with its stack; the run continues |
Driving it from Claude Code
| surge | |
|---|---|
| In this repository | .claude/skills and .claude/agents — open it in Claude Code and they are there, nothing installed |
| Skills | perf-plan · perf-setup · perf-run · perf-gate · perf-capacity · scenario-reference |
| Agents | perf-author drafts a scenario from a codebase · perf-analyst diagnoses a finished run |
| In another project | the same files as a plugin: claude plugin marketplace add <repo> then claude plugin install surge@surge |
| Kept honest | scripts/sync-plugin.sh copies one onto the other; a test fails the build if they drift |
Deliberately not here
- An extension plugin API. surge is a Go library; a new protocol is an import and a
c.Time, not an interface to learn and a plugin ABI to version. (The Claude Code plugin above is a consumer of surge, not an extension point inside it.) - A Kubernetes operator. A run is a Job that exits 0 or 1, which Kubernetes already models. See deploy/kubernetes.
- A WebSocket ping helper. A pong is only processed while a read is in flight, so a
Pingon an idle connection waits for a reply nobody is listening for.WS.Requestmeasures the round trip that matters. - Single-user frontend profiling. A browser devtools session answers that. surge drives real browsers to measure the page under load, which is a different question — see browser.
- Closed binary protocols. Some desktop-client and remote-session protocols have no public specification. An
exec:step times the tool that does speak one; surge will not claim to speak it itself. - Mutating percentile buckets after the fact.
Validateruns before the sample is recorded, so the report never needs correcting.