how it works
A small controller around stock k6.
LiteTraffic is a Python CLI. There is no server, database or background daemon: each run seeds its own state, drives journeys with a pinned k6, reads back what changed, and writes the evidence to disk.
interactive preview · examples/checkout
Seed a world, then drive it.
A scenario names its users, the state they start from, the pace they arrive at, and what must hold afterwards. LiteTraffic sets up that state, runs the journeys on a seeded schedule with stock k6, and reads back the result.
{
"name": "checkout",
"actors": [{"class": "buyer", "count": 8}],
"fixtures": {"recipe": "owned-shop"},
"schedule": {
"unit": "journeys_per_second",
"phases": [
{"name": "warmup", "seconds": 1, "rate": 1},
{"name": "measure", "seconds": 5, "rate": 2},
{"name": "recovery", "seconds": 1, "rate": 1}
]
},
"assertions": ["one_effect_per_payment",
"accepted_orders_persist",
"order_totals_match"],
"budgets": {"max_seconds": 20, "max_requests": 40}
}- Seeding, done
- Traffic, done
- Observing, done
- Verdict, done
run log
- preparingFixture owned-shop created for this run
- runningk6 v2.2.0 drove 12/12 journeys, 36 requests
- observingObserver owned-order-ledger read the final state
- finalizing3 of 3 assertions hold. report.html written
This replays the documented first run of examples/checkout against its demo server, with the timing shortened. A real run writes every step to .litetraffic/runs/<run_id>/.
A scenario is a directory you review
my-scenario/
├── manifest.json # declared shape, schedule, assertions, budgets
└── journeys.js # k6 script that performs the journeys and emits evidenceThe manifest names the actors, the starting state (fixtures), the schedule, the assertions that must receive evidence for a pass, an optional final observation, and the budgets the run must fit. Unknown fields are rejected. The journey script is ordinary k6 JavaScript, and it is trusted code: review it as you would any test.
litetraffic inspect validates the manifest, checks the script and its relative imports stay inside the directory, resolves the schedule for a seed and checks the worst case against the budgets. It sends nothing to your app. The fastest start is to copy the bundled example closest to your case, or to generate a tenant-isolation scenario with litetraffic init.
One run, step by step
- Load. The manifest is parsed, the bundle digest is computed over the manifest, the script and every file it imports, and the budgets are checked against the worst case.
- Preflight. k6 must be exactly v2.2.0 and the target a credential-free HTTP(S) URL. An E2B sandbox ID and port can be resolved to a URL.
- Fixture. Optionally, a run-owned record is created over HTTP, setup commands run, or a pool hands each journey its own item.
- Engine. k6 runs a temporary copy of the scenario in its own process group, with a deadline derived from
max_seconds. - Evidence.
LT_EVENTlines are parsed and checked against the run ID; metrics are aggregated (p50, p95, max, error rate, iterations). - Observation. Optionally, a final read-only
GETis compared by JSON Pointer, polling until it converges when the app settles asynchronously. - Cleanup. Owned fixtures are deleted and teardown commands run, even after a failure. A failed cleanup is never hidden: it makes the verdict
error. - Verdict. Assertions, completeness, lifecycle and budgets combine into
pass,fail,inconclusiveorerror, and a standalonereport.htmlis written.
Seeded schedules
Traffic is measured in journeys per second. Explicit phases admit ceil(seconds × rate) journeys each. Profiles compile into phases from the seed, so the same seed always yields the same schedule:
| Profile | Shape |
|---|---|
spiky | Steady baseline with short spikes, one per equal window |
random_bursts | Mostly quiet (the rate may be 0) with seeded bursts |
sustained_burst | Linear ramp, plateau, return to baseline |
The script receives the resolved phases as LT_SCHEDULE_JSON and turns them into a k6 ramping-arrival-rate executor. The seed is recorded with the run, so a rerun with the same seed sends the same pressure.
Evidence through logs
Scripts report each assertion on stdout, so no k6 extension is needed. Every journey writes one line per declared assertion, keyed by the journey that produced it:
LT_EVENT {"schema_version": 1, "type": "assertion", "run_id": "<run_id>",
"assertion": "one_effect_per_payment", "passed": false,
"logical_key": "<run_id>-traffic-0", "expected": 1, "actual": 2}An assertion passes only when every sample passes, every sample has a key, and the number of distinct keys equals the planned journeys. One journey reporting twice cannot stand in for a journey that reported nothing. The optional expected, actual and detail fields never change the verdict; they make a failure readable.
Where it runs
Against a local app, a CI or preview URL, or an existing E2B sandbox. Your app must already be running. URL targets must not carry credentials, link-local and cloud-metadata addresses are refused before a request is sent, and k6 runs with --max-redirects 0. Point writes only at data you own: a local app, an isolated preview, or run-owned fixtures.
Design choices
- Stock k6, pinned. No custom engine build; exact version pinning keeps metrics and behavior comparable.
- Evidence through logs. Assertions travel on stdout, so any k6 script can report them.
- Verdict separate from lifecycle. A run can finish and fail, or time out and still record a definite failure.
- Local artifacts. Everything a verdict depends on is written to disk for review and comparison.
Read the full architecture and scenario reference.