Help us shape the future of Go! Take the 2026 Go Developer Survey

stressy

command module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 2 Imported by: 0

README

stressy

CI Coverage Latest release Go version

A CPU stress tool. It loads as many CPUs as you ask it for with bcrypt hashing and reports the rate, so the same run on two nodes is a comparison.

stress-ng has 300 stressors; stressy has one and deploys anywhere — a static binary of about two megabytes for eight OS/architecture targets, and a FROM scratch image with no base layer, no package manager, no shell and a non-root UID.

Install

go install github.com/felipeneuwald/stressy@latest
docker pull ghcr.io/felipeneuwald/stressy:latest

Or a prebuilt archive from the releases — tar.gz per unix target, zip for windows, each holding the binary, the licence and this README:

# It extracts flat, so naming the binary keeps its LICENSE and README out of the way
tar -xzf stressy_Linux_x86_64.tar.gz stressy
./stressy -t 5s

Usage

# One worker until interrupted
stressy

# Four workers for five minutes
stressy -w 4 -t 5m

# Any Go duration
stressy -t 1h30m

# A progress line every 30 seconds
stressy -t 30m -r 30s

Every setting is a flag, and none of them is inferred. -w defaults to 1, so a bare stressy loads one CPU on a laptop, in a container and in a pod alike; to load the whole machine, say so — stressy -w $(nproc) on Linux and FreeBSD, stressy -w $(sysctl -n hw.ncpu) on macOS, which has no nproc, and stressy -w $env:NUMBER_OF_PROCESSORS in PowerShell on Windows. stressy reads no environment variable, no config file and no positional argument, so a command line is the whole of what a run was given.

Output

A run says what it is about to do, why it stopped, and what it did:

$ stressy -w 4 -t 60s
Starting CPU stress test with 4 workers for 1m0s
Timer expired, shutting down; waiting for every worker to finish the hash it is on...
Computed 1324 hashes in 1m0.101s (22.0 hashes/s, 4 workers)

A run with no -t says how to stop it, and still reports what it managed:

$ stressy -w 4
Starting CPU stress test with 4 workers indefinitely
Press Ctrl+C or send SIGTERM to stop. Use --help for additional information
^C
Received SIGINT, shutting down; waiting for every worker to finish the hash it is on...
Computed 412 hashes in 18.734s (22.0 hashes/s, 4 workers)

That wait is the gap between the last two lines, and it is worth knowing how long it can get. A run does not stop until every worker has finished the bcrypt hash it is inside: one hash while the workers fit in the CPUs on offer, and roughly --workers divided by that many where they do not, because from there the hashes in flight finish in series. Measured on 18 cores, -w 18 stopped about 0.2s after the signal and -w 2000 took about twenty seconds. Once the shutdown line has printed, a second signal is not caught — it ends the process where it stands — so a drain you are not prepared to wait out is one more Ctrl-C, at the cost of the summary line. That line is the gate, and the goroutine printing it is one the workers are starving: on the same 18 cores it followed the signal by 0.05s at -w 18 and by 2.6s at -w 1440. A signal sent before it is absorbed.

Because bcrypt at a fixed cost is constant work per hash, that rate is a crude cross-node benchmark: a node hashing 30% slower is a finding.

That cost is 12, and it is fixed for the whole of 1.x. It is the unit every number above is quoted in, so a figure recorded today and a figure recorded a year from now are the same measurement; moving it would halve or double every published number, which makes it a major version bump and nothing less. It is not bcrypt.MaxCost, which is about 26 hours a hash: no harder on a core, and long enough that a worker would never notice the run had ended.

Between those lines a run says nothing, so -r, --report fills the gap:

$ stressy -w 4 -t 2m --report 1m
Starting CPU stress test with 4 workers for 2m0s
1m0.001s elapsed, 1320 hashes, 22.0 hashes/s
2m0.001s elapsed, 2636 hashes, 22.0 hashes/s
Timer expired, shutting down; waiting for every worker to finish the hash it is on...
Computed 2640 hashes in 2m0.093s (22.0 hashes/s, 4 workers)

The interval has to be 1s or longer, and no longer than --timeout where there is one: below a second a run spends itself formatting rather than hashing, and past the timeout the ticker never fires, which is what -r 1s mistyped as -r 1m looks like — three lines, exit 0 and nothing to correct it by. Both are rejected before any worker starts. A run with no -t outlives every interval, so it takes any: stressy -r 5m reports until you stop it.

The output is the interface

There is no --json. The lines above are what a script reads, and their wording is stable for 1.x: rewording one is a breaking change and takes a major version bump. Two of them carry a rate, so a script after the figure for the whole run matches the summary — the line that starts with Computed — rather than hashes/s, which every progress line carries too.

Containers
# Bounded run: 30 seconds, then the container exits on its own
docker run --rm ghcr.io/felipeneuwald/stressy:latest -t 30s

# Two CPUs' worth of load for five minutes
docker run --rm --cpus 2 ghcr.io/felipeneuwald/stressy:latest -w 2 -t 5m

Both are bounded, deliberately: with no timeout, docker run -d leaves a container hashing until somebody runs docker stop.

The worker count is -w and nothing else — no CPU quota is read, so --cpus 2 without a -w 2 loads one CPU and pays for two. Match -w to the limit: above it the extra workers buy throttling rather than load, below it the limit is partly idle.

In Kubernetes a Job is the shape this fits: one pod, run to exit 0, recorded as finished — which makes -t and the exit codes load-bearing, since a pod evicted, preempted or kubectl deleted part-way through exits 143.

apiVersion: batch/v1
kind: Job
metadata:
  name: stressy
spec:
  # A run that failed should not be retried indefinitely at full CPU.
  backoffLimit: 0
  # Clear the finished Job an hour after it completes.
  ttlSecondsAfterFinished: 3600
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: stressy
          image: ghcr.io/felipeneuwald/stressy:latest
          # -w matches the cpu limit below; nothing derives one from the other.
          args: ["-w", "2", "-t", "60s"]
          resources:
            limits:
              cpu: "2"
              memory: 64Mi
            requests:
              cpu: "2"
              memory: 64Mi
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
            seccompProfile:
              type: RuntimeDefault
      securityContext:
        runAsNonRoot: true
kubectl apply -f stressy-job.yaml
kubectl logs -l batch.kubernetes.io/job-name=stressy

Those four securityContext fields are what the restricted Pod Security Standard requires, and the whole of it: runAsUser is not among them, because the image already runs as UID/GID 65532. It is FROM scratch, so there is no shell to kubectl exec in with and stdout is the only view you have of a run — which is what --report is for, and why the shutdown line names the signal: an evicted pod logs Received SIGTERM, which is the 143 it goes on to exit with. :latest only ever points at a full release.

That 143 has a deadline on it, and it is the wait described above. A pod's default terminationGracePeriodSeconds is 30 and docker stop's default grace is 10, against a wait that grows with --workers: a -w far above the CPU limit can outlast either, and the container is then SIGKILLed part-way through it — 137, no shutdown line and no summary, and none of the 143 this section is built on. Matching -w to the limit, as the manifest does, keeps the wait to about one hash; a -w deliberately above it wants a terminationGracePeriodSeconds long enough to cover the wait.

Available Flags
  • -w, --workers: Number of parallel workers (must be 1 or greater). 1, the default, on every machine: nothing is read from the core count, the CPU affinity mask or a cgroup limit, so the number a run uses is the number you typed
  • -t, --timeout: How long to run, as a duration such as 30s, 5m or 1h30m. 0, the default, runs until interrupted
  • -r, --report: Print a progress line this often — elapsed time, hashes computed and rate. Takes the same duration spellings --timeout does, no shorter than 1s and, on a bounded run, no longer than --timeout. 0, the default, prints none, which is what a run has always done
  • -h, --help: Show help information
  • -v, --version: Show version information
Exit codes
Code Meaning
0 The run served the whole --timeout it was given
1 The configuration was rejected — an unknown flag, an unparseable or out-of-range value, an unexpected argument — and no work was done
130 SIGINT cut the run short, which is 128 + 2 and what Ctrl-C sends
143 SIGTERM cut the run short, which is 128 + 15 and what docker stop, a kubectl delete pod and a node drain send

Each of those is a code stressy chose and exited with. A second signal is not one: from the shutdown line on, stressy has stopped handling signals, so the next one kills the process outright and the status is whatever the system reports for a run that never exited — no summary line, and no code of stressy's own. That is the escape hatch for a drain too long to wait out, and the reason a run cannot become unstoppable.

Building from Source

git clone https://github.com/felipeneuwald/stressy.git
cd stressy
go build
./stressy -t 5s

Contributing

CONTRIBUTING.md has the checks CI runs and the conventions.

Security

Please do not open a public issue for a vulnerability. SECURITY.md says what is in scope and how to report privately.

License

MIT — see LICENSE.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
internal
stressy
Package stressy is the stressy command: the flag grammar it publishes and the CPU stress test that grammar configures, which worker goroutines run by hashing bcrypt.
Package stressy is the stressy command: the flag grammar it publishes and the CPU stress test that grammar configures, which worker goroutines run by hashing bcrypt.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL