A correctness fuzzer for checkout and inventory races. You point it at a backend, it fires N requests at the same instant into a critical section, and then it checks a business invariant against the server's observable state — for example, you can never sell more units than ever existed. Every run prints a seed so a violation can be replayed.
k6, Locust, and friends answer "how many requests per second can this take, and how slow does it get?" oversell asks a different question: "when two requests hit the last item at the same moment, does the server sell it twice?" It isn't trying to push volume. It releases a small batch of requests together to land them inside the same check-then-act window, then judges the result against an invariant. Pass/fail, not a latency histogram.
go install github.com/imharjot/oversell@latest
There's a deliberately-buggy backend bundled in, so you don't need your own server to see a violation.
Terminal one — start the buggy demo backend (one unit in stock):
oversell serve --kind buggy --stock 1
# buggy backend listening on http://127.0.0.1:8080 (stock=1)
Terminal two — fire 50 simultaneous checkouts at it:
oversell run --target http://127.0.0.1:8080 --scenario checkout-last-item --concurrency 50 --seed 42
You'll get something like:
seed 42 | scenario checkout-last-item | concurrency 50 | target http://127.0.0.1:8080
VIOLATION no-oversell
units_sold=50 exceeds initial_qty=1 (server-reported oversold=49)
send order (slot->task): [16 36 27 4 45 44 19 33 7 ...] ... (50 total)
replay: oversell run --target http://127.0.0.1:8080 --scenario checkout-last-item --concurrency 50 --seed 42
The process exits non-zero on a violation, so it drops straight into CI.
Now swap the backend for the locked version and run the exact same command:
oversell serve --kind correct --stock 1
seed 42 | scenario checkout-last-item | concurrency 50 | target http://127.0.0.1:8080
PASS no invariant violated
final state: units_sold=1 stock=0 initial=1
A target needs two endpoints for v1:
-
POST /checkout— attempt to buy one unit. The status code doesn't matter to the tool; a sold-out409is a fine outcome. The verdict comes from state. -
GET /state— return JSON the oracle can check. Forcheckout-last-item:{ "stock": 0, "units_sold": 1, "oversold": 0, "initial_qty": 1 }Only the fields an invariant references need to be real.
checkout-last-item is the one wired up in v1. The Scenario interface
(internal/scenario) has two halves — the per-worker request and the
invariants — so adding one is small. Planned next, sketched in
internal/scenario/next.go:
double-spend— one balance, two concurrent debits.coupon-double-redeem— a single-use coupon redeemed by N carts at once.reservation-leak— hold/confirm/release under churn, asserting nothing gets stuck in a phantom hold.
Worth being upfront about:
- It controls request release, not the server's scheduler. oversell can get N requests onto the wire as close to simultaneously as the OS and runtime allow, but it can't dictate how the server interleaves them once they land. Whether a race actually fires still depends on the target's internal timing.
- A clean run is not a proof. Not seeing a violation means this seed and this concurrency didn't trigger one — not that the code is race-free. Vary the seed and concurrency.
- v1 observes state over HTTP. The oracle reads a
/stateendpoint. If your invariant can't be observed that way (it lives only in a DB, say), v1 can't check it yet. - It's a single-process client. Truly simultaneous arrival is bounded by one machine's networking. For most check-then-act bugs that's plenty; for very tight windows it may not be.
See DESIGN.md for how the barrier, oracle, and replay work.
go build ./...
go test ./... -race