stressy

A CPU stress tool. It loads every CPU it is given 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 2.8 MB
static binary for twelve 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 binary.
Usage
# Load every usable CPU until interrupted
stressy
# Four workers for five minutes
stressy -w 4 -t 5m
# Any Go duration; a bare number is seconds, so pre-0.4 command lines still work
stressy -t 1h30m
stressy -t 60
# A progress line every 30 seconds
stressy -t 30m -r 30s
# The same settings from the environment
export STRESSY_WORKERS=4
export STRESSY_TIMEOUT=5m
stressy
A flag given on the command line beats its environment variable, and an empty
variable counts as unset — so STRESSY_WORKERS=${WORKERS} with WORKERS
undefined leaves the default in place rather than failing the run.
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 60s
Timer expired, shutting down...
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 signal, shutting down...
Computed 412 hashes in 18.734s (22.0 hashes/s, 4 workers)
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.
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...
Computed 2640 hashes in 2m0.093s (22.0 hashes/s, 4 workers)
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, configured by the environment
docker run --rm --cpus 2 -e STRESSY_TIMEOUT=5m ghcr.io/felipeneuwald/stressy:latest
Both are bounded, deliberately: with no timeout, docker run -d leaves a
container loading every CPU it can reach until somebody runs docker stop.
The worker count is read from the cgroup CPU quota, not the host's core count, so
--cpus 2 on a 16-core host starts 2 workers and limits.cpu: "2" in a pod spec
starts 2 as well — -w is redundant there, and above the limit it buys
throttling rather than load. The floor is 2, so --cpus 1 starts 2.
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
args: ["-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. :latest only ever points at a full release.
Available Flags
-w, --workers: Number of parallel workers (must be 1 or greater). Defaults to the number of CPUs this process can use — the host's core count, narrowed by the CPU affinity mask, by a cgroup CPU limit if there is one, and by the GOMAXPROCS environment variable if it is set. stressy --help prints the number for the machine you run it on
-t, --timeout: How long to run, as a duration such as 30s, 5m or 1h30m. A bare number is read as seconds, so -t 60 still means one minute. 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, a bare number included. 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 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 |
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.