Test whether concurrent API requests violate application correctness rules.
One item in stock. Two purchases succeed. ConcurTest catches the oversell and reduces the failure to two concurrent requests. Try the demo.
ConcurTest is a Go command-line tool that sends concurrent HTTP requests and checks a declared correctness rule. YAML scenarios define the requests and the invariant. Reports show the evidence and a command to reproduce a failure.
Features include:
- Bounded concurrency and repeated trials.
- Ordered setup requests before every trial.
- Checks against observed JSON values or recorded HTTP responses.
- Failure reduction, text reports, and structured JSON reports.
Requests can succeed individually while producing an incorrect result together: oversold inventory, duplicate checkout, or repeated state changes. These failures can occur even when the application has no data races. ConcurTest tests the application's behavior through its HTTP interface.
Requires Go 1.27 or newer:
go install github.com/eumarumar/concurtest/cmd/concurtest@v0.1.0Add Go's executable directory (go env GOBIN, or $(go env GOPATH)/bin when
GOBIN is unset) to PATH.
If concurtest is not found after installation, run this on Linux or macOS:
concurtest_bin="$(go env GOBIN)"
export PATH="${concurtest_bin:-$(go env GOPATH)/bin}:$PATH"
concurtest --helpThis updates PATH for the current terminal only. To keep concurtest available
in future terminals, add these two lines to your shell startup file:
concurtest_bin="$(go env GOBIN)"
export PATH="${concurtest_bin:-$(go env GOPATH)/bin}:$PATH"Choose the file for your shell and operating system. Run echo "$SHELL" if you
are unsure which shell you use.
| Shell | Startup file |
|---|---|
| Zsh (Linux or macOS) | ~/.zshrc |
| Bash (Linux) | ~/.bashrc |
| Bash (macOS) | ~/.bash_profile |
Create the file if it does not exist. Open a new terminal, then run
concurtest --help to check that the change worked.
Download a compiled archive from GitHub Releases. Choose the archive for the operating system and processor:
| Computer | Archive |
|---|---|
| Linux x86-64 | concurtest-linux-amd64.tar.gz |
| Linux ARM64 | concurtest-linux-arm64.tar.gz |
| macOS Intel | concurtest-darwin-amd64.tar.gz |
| macOS Apple Silicon | concurtest-darwin-arm64.tar.gz |
| Windows x86-64 | concurtest-windows-amd64.zip |
On Linux or macOS, install into ~/.local/bin. This example uses Linux x86-64;
substitute the matching filename from the table:
mkdir -p "$HOME/.local/bin"
tar -xzf concurtest-linux-amd64.tar.gz -C "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
concurtest --helpTo keep the command available in future terminals, add this line to the startup file for your shell from the table above:
export PATH="$HOME/.local/bin:$PATH"Open a new terminal, then run concurtest --help to check that the change worked.
On Windows, use PowerShell:
Expand-Archive .\concurtest-windows-amd64.zip -DestinationPath .\concurtest
.\concurtest\concurtest.exe --helpAdd the extracted binary's folder to PATH to use concurtest from any directory.
GitHub's Source code (zip) and Source code (tar.gz) downloads contain the repository, not compiled binaries. See GitHub's release documentation.
The included inventory service has a repeatable overselling bug. Start it with Node.js or Go 1.27 or newer, with no credentials or external services. With Node.js and a downloaded ConcurTest binary, you do not need Go or npm packages.
Get the example files:
git clone https://github.com/eumarumar/concurtest.git
cd concurtestIn one terminal, choose either version of the demo.
With Node.js:
node examples/vulnerable-inventory/node/server.jsWith Go:
go run ./examples/vulnerable-inventoryIt listens on 127.0.0.1:8080. In a second terminal:
concurtest run examples/vulnerable-inventory/observation-scenario.yamlThe scenario resets stock to one before each trial, then sends four purchase
requests at concurrency four. Two purchases succeed, leaving stock at -1.
Expected output, excerpted:
ConcurTest · inventory oversell
VIOLATED
10/10 trials demonstrated the violation.
Invariant
final stock must be non-negative
Expected $["stock"] >= 0
Observed $["stock"] = -1
Reduction finds the same failure with two attempts at concurrency two.
Exit code 1 is expected: the scenario demonstrated the intended bug.
Use disposable local targets; the requests change application state.
Each scenario declares one invariant:
| Check | YAML fields |
|---|---|
| Inclusive integer bounds | json_integer_path with minimum, maximum, or both |
| Exact integer value | json_integer_path with equals |
| Change from the trial's baseline | json_integer_path with change |
| Maximum successful operation responses | maximum_successful_attempts, optional successful_status_codes |
Integer checks require an observation request. History checks count qualifying HTTP responses; success defaults to any 2xx status. See the scenario reference for syntax and evaluation rules.
| Example | Scenarios | Preparation |
|---|---|---|
| Inventory regression demo | Observation, history | Local Node.js or Go service; no credentials |
| Juice Shop case study | Stock observation, checkout history | Local Juice Shop, customer and accounting credentials, inventory access |
Run scenarios against a shared target sequentially. The Juice Shop guide covers setup requirements and includes evidence from a previously captured run.
setup accepts one request or a list of named requests. Steps run sequentially
in YAML order before every trial, including reduction trials. Every step must
return 2xx; a failed step stops that trial before test operations run.
Setup must establish the preconditions relevant to the invariant. Completed steps are not rolled back. A fresh baseline measures change within a trial; it does not remove side effects from previous trials. See setup syntax and behavior.
Set execution.reduce: true to search smaller attempt and concurrency settings.
Reduction requires setup and at least three trials. It runs after a majority
of trials violate the invariant, with no errored or inconclusive trials.
The result is the smallest observed failure; smaller failures may still exist.
Reports include a reproduction command, for example:
concurtest run --attempts 2 --concurrency 2 --no-reduce examples/vulnerable-inventory/observation-scenario.yamlUse --verbose for retained trial evidence. See the
reduction reference for search limits.
| Exit code | Meaning |
|---|---|
0 |
Every trial passed |
1 |
At least one trial demonstrated a violation |
2 |
No violation demonstrated; the run errored, was inconclusive, or was interrupted |
A passing run means the declared invariant held across all tested trials. This provides evidence of correctness with respect to that invariant under the tested conditions, but does not guarantee correctness across every possible request timing, input, or application state. A violated run records evidence that the declared invariant was broken in an observed execution.
Use the installed binary in CI and select JSON for structured evidence:
concurtest run --format json examples/vulnerable-inventory/observation-scenario.yaml > report.jsonReport format does not change exit codes. The JSON schema defines the report contract.
- HTTP/JSON only; one repeated operation and one invariant per scenario.
- Requests are static; YAML values do not expand environment variables or capture values from earlier responses.
- Setup controls trial state; automatic rollback and resource isolation are not provided.
- Net stock changes can hide duplicate effects, and successful HTTP responses alone do not prove business outcomes.
Licensed under Apache 2.0. For contributions, read the architecture, keep changes focused, and run:
go test ./...
go test -race ./...
go vet ./...For inventory demo changes, also run the Node.js server tests:
node --test examples/vulnerable-inventory/node/server.test.jsWith node installed, the Go test suites also check both scenarios and failure
reduction against isolated Node.js instances. Those integration tests skip when
node is unavailable.
