kubernetes-native fuzz + load testing

Fuzz and load test your JVM web apps — unmodified, in Kubernetes.

Instrument a running app with a BasquinTarget — no rebuild, no image changes. Run a coverage-guided fuzz campaign to discover the inputs that stress it, then replay those exact inputs under load. And because the bug oracle is availability — latency budgets, heap retention, thread/executor leaks — you catch the apps that don't crash but degrade.

Basquin driving an unmodified JPetStore pod in a kind cluster
250 requests against an unmodified JPetStore pod — live coverage % (281/6368 edges of the pod's own code, pulled from its JaCoCo agent), 96 invariant finds harvested server-side through the valve, 0 crashes.

Instrument once, then run campaigns

Two custom resources in group basquin.dev/v1alpha1. You instrument an app once, then run any number of bounded tests against it.

// long-lived

BasquinTarget — instrument

Patches an unmodified app Deployment to load the agents via an initContainer + shared volume. Fully reversible — deleting it restores the Deployment byte-for-byte.

// ephemeral

BasquinCampaign — test

Drives the instrumented target and aggregates results into status. Runs in one of two modes: explore (coverage-guided fuzzing) or load (corpus replay at volume).

ModeWhat it doesBounded by
explore Coverage-guided fuzzing — keeps inputs that reach new code, emits the interesting ones as a corpus iterations or duration
load Replays that saved corpus at a fixed concurrency, reporting throughput and latency percentiles duration

Fuzz to discover the interesting states, then hammer those states under load. Install with Helm, drive it with the basquin CLI — see the operator guide.

Why it's different

Fuzzers like Jazzer and JQF are excellent, but their oracle is "did it throw / crash." Load tools like k6 and Gatling measure throughput but know nothing about what's happening inside the JVM. Basquin sits in the gap — and the corpus carries from fuzzing straight into the load test.

// oracle

Availability is the bug

An iteration is interesting if it exceeds a latency budget, grows the heap, or leaks a thread/executor — not only if it throws.

// isolation

Cleanliness enforced, not assumed

Each iteration runs inside strict begin/end boundaries; leaked non-daemon threads and un-shut executors are detected and reported with stacks. Contamination becomes obvious.

// unmodified

Works on apps you can't change

A Tomcat valve wraps every request of an unmodified third-party WAR — no code edits, no repackaging — and one namespace-free jar runs on Tomcat 9 (javax) and 10+ (jakarta).

Signal quality is the one rule

The project has exactly one bar for what ships: a change has to improve iteration cleanliness, reproducibility, or signal quality — find real availability pathologies, faster. Crashes are real crashes: a target declares its expected input rejections via a CrashClassifier, so a parser throwing on bad input is counted as rejected, not a crash. Only genuine faults — an unhandled exception, a 5xx — count.

What it found in JPetStore, unmodified

Running inside JPetStore's JVM via the valve, with no changes to the app:

SignalExample
Latency spikecold first-catalog request 531ms (budget 20ms)
Heap growthup to 44MB allocated on a single request
Per-request coststeady multi-hundred-KB retention across catalog routes
Basquin exploration panel: execs/sec, corpus, finds, coverage
Exploring an unmodified JPetStore over HTTP — 220 requests, 0 crashes (it's robust), 48 invariant finds harvested server-side via the valve.

What's in the box

// invariants

Availability invariants

Latency, heap-delta, and thread-delta thresholds, each with hard-fail or soft-signal modes and stack evidence on violation.

// exploration

Coverage-guided exploration

A JaCoCo agent runs in the target's JVM; the driver reads real coverage over the wire and keeps the inputs that reach new code. Routes and value structure are data — a request grammar, not code.

// correlation

Correlated write paths

A sequence captures a CSRF token or a randomized anti-automation field out of one response and substitutes it into the next, so exploration reaches authenticated writes — not just the pages a crawler can see. The corpus stores the recipe, never the token.

// jvmti

JVMTI native agent

Event-driven thread lifecycle tracking (ThreadStart/ThreadEnd) — no polling, no safepoint stack walks — with a ThreadMXBean fallback. One jar, JDK 17 & 21.

// valve

Tomcat valve

Wraps every request of an unmodified WAR. A single namespace-free jar runs on both javax.servlet and jakarta.servlet Tomcat lines.

// dashboard

Decoupled web dashboard

A standalone process — never embedded in a driver. Any number of drivers push status and findings, keyed by campaign id, for a fleet view with drill-down. JDK httpserver, binds 127.0.0.1 by default.

// kubernetes

Kubernetes operator

Two CRDs, namespaced RBAC (never a ClusterRole), reversible in-place instrumentation of unmodified images, and a per-campaign dashboard. Installed with Helm, driven by the basquin CLI.

// load

Load / soak mode

Replays the corpus a fuzz run emitted at a fixed concurrency for a duration, reporting throughput and p50/p90/p99/max latency, plus heap and thread drift across the run.

Point it at your app

Install the operator with Helm and instrument a running Deployment — no rebuild — then fuzz it and replay the findings under load. The harness also runs standalone if you're not on Kubernetes.