Every tester knows the feeling: you write a beautiful, deterministic scripted test. It clicks precisely, types deliberately, and asserts exactly what you expect. And it passes, every time. Meanwhile, in production, some user with an unusual browser window size and a distractible clicking habit manages to break the exact feature your test "covered."
Scripted tests are great at verifying what you expect. They're terrible at finding what you don't. That's the gap chaos testing — sometimes called monkey testing — was built to fill.
What monkey testing actually is
Monkey testing is the deliberately un-scripted cousin of normal testing. Instead of telling the browser "go here, click this, type that," you let a swarm of pseudo-random interactions loose on the page: random clicks, random scrolling, random form submissions, random navigation. The "monkey" doesn't know what the app is supposed to do. It just pokes at everything and watches whether the app survives the poking.
The name comes from the infinite monkey theorem analogy — give enough monkeys typewriters and you get Shakespeare. Give enough clicks to a web app and you get crashes. That's the idea: at volume, seemingly random interactions start to hit the unhit paths.
In Qalizer, the monkey is the open-source Gremlins.js library, wrapped into a testing mode with bounded, explicit settings, and connected to the findings system. Every run starts with a baseline check: the target page is opened in a clean browser context first, and its console errors and failed requests are recorded before any gremlin touches it. That baseline matters more than it sounds — it separates "this page was already broken" from "the monkeys broke it".
Why random storms find bugs that scripts miss
Scripted tests fail in predictable ways: an assertion doesn't hold, an element times out. They generally fail where you pointed them. A monkey test fails in a completely different dimension — often with the actual browser crashing, a modal freezing, the page going unresponsive, or a state getting corrupted.
The classic scenario: a slideshow carousel. Your scripted test clicks "next" five times and asserts the fifth slide is visible. Passes. Now let the monkey click "next" and "prev" in a rapid, irrational rhythm, interleaved with random slider drags, followed by a click on a navigation link while the animation is mid-flight. On a real app, that's a race. And races are exactly what structured tests almost never trigger, because structured tests are polite. The monkey has no manners.
That's the core value proposition: chaos testing finds the bugs that live in the messy middle — the transitions, the races, the error paths, the rapid-interaction edge cases that a rational human would never deliberately construct.
There's also the sheer coverage argument. A human writes maybe a hundred deliberate interactions per session. A monkey fires thousands in the same time, covering interaction combinations no human would ever think to try.
Choosing duration and interaction volume
The most common misconception is that chaos testing has to be a chaotic free-for-all. It doesn't, and it shouldn't be. The whole point of doing it thoughtfully is to control how chaotic, so you can actually act on the results.
Qalizer's gremlins mode is deliberately bounded. There is no "run until it stops" option, because an unbounded horde is a resource problem, not a test. The settings are:
Duration. How long the storm runs: 5, 10, 30 or 60 seconds. Anything else is coerced back to the 10-second default. A 5-second burst is a smoke test of resilience. A 60-second run accumulates far more interactions and is where the "leave this page open and it eventually dies" class of bug starts to show up. One minute is the maximum — this is not a soak-test tool, and a memory leak that only appears after an hour is not something a gremlins run will catch for you.
Max actions. This is the real "intensity" dial: how many gremlin actions may fire inside the duration. It is an action budget, not a number of browsers. Two things bound it:
- On the shared server pool the budget is capped at 200 actions per run, and the cap is enforced where the actions execute, not just in the form. A bigger number on a shared run is refused rather than silently reduced.
- On your own local runner there is no product-level cap — you can enter any sane positive number and the runner accepts it unchanged. The duration still stops the run, and the machine's own resources still apply.
The horde stops at whichever comes first: the action budget is reached, or the duration elapses. That ordering matters more than it looks. An earlier version let the browser-side timer inflate the action count to fill the requested duration, which meant a 200-action budget could execute a thousand actions — a silent breach of the product cap. The count you set is now a ceiling.
Because of that, 200 actions over 60 seconds is a sparse storm: at a 100 ms delay, 200 actions finish long before the minute is up, and the run reports duration_elapsed or action_budget so you can see which limit ended it. If you want a dense storm you either raise the budget (local runner) or shorten the duration.
Delay. The gap between actions: 1 ms, 10 ms or 100 ms. Short delays produce tighter, more overlapping interactions, which is how you provoke races; longer delays give the page time to settle between actions and produce cleaner evidence. The form offers those three values, and a saved scenario configuration is clamped to a minimum of 100 ms, so a saved config can never turn into a runaway. The API itself accepts any positive millisecond value.
Species. Which kinds of gremlin are allowed to act: clicker, scroller, toucher, formFiller and typer. All five are enabled by default. Turning off formFiller and typer is the first thing I do on an app where submitting a form sends a real email.
A sensible progression is: the smallest budget on a staging copy — well under the shared-pool cap for the first run — then raise the action budget as the app proves robust. What you're really doing is increasing the probability that the monkey finds a genuinely broken path, without overwhelming yourself with so many findings you can't triage them.
Run it where it's allowed to break
This is the part I'd put first if I could, and it's why the mode has such narrow limits.
Gremlins click things. That is the entire point, and it means a gremlin run on a real production site can submit real forms, send real emails, add real items to real carts, trigger real password-reset flows and, on an app with side effects behind buttons, change real data. None of that is a bug report you want.
Point it at an authorized staging or test environment you own or have written permission to test, seeded with disposable data. If you must test something close to production, use an account and environment dedicated to testing, and turn off the species that can submit forms. Similarly, don't aim a horde at a site that isn't yours — the request comes from wherever the run executes, and the owner will see it as traffic, or worse, as an attack.
There is a second, quieter limit: gremlins are a job-queue family, not a browser scenario. They are not startable from the CI API and there is no scheduler integration, so "regularly" means putting it on a release checklist rather than on a timer. Where a run executes depends on what you asked for:
auto(the default) uses your own local runner when one is eligible, and otherwise the shared server pool.serveralways uses the shared pool, with the 200-action cap.localonly uses your own runner. If it is offline, stale or lacks browser capability, the run is refused with a reason — it is never quietly moved to the shared pool.
The run records which location it actually used, so read that field before you generalise: a successful horde on a fast local machine is evidence about your machine, not proof that the same configuration behaves the same way on the shared pool. If a finding looks like a capacity problem rather than an app problem, check where it ran first.
How to read the resulting findings
Here's where a lot of teams go wrong with chaos testing, and why I built the reporting the way I did. A raw monkey test that says "something crashed" is nearly useless. You need to know what happened, where, and ideally why. Otherwise you spend all day chasing ghosts.
When Qalizer runs a gremlins session, it collects structured evidence around the run and turns it into findings:
- Console and page errors — the JavaScript exceptions the storm triggered, with their text and location, which point at the code path that broke.
- Network failures — failed requests and HTTP errors the storm ran into, with tracking/analytics noise filtered out and tokens or session parameters redacted from the recorded URLs before anything is stored.
- FPS drops — the run samples frame rate and reports the samples that fell below 30 FPS. A page that only stutters when a storm is running is telling you something about its rendering.
- The pre-run baseline — findings that were already present before the first gremlin fired are marked as baseline findings, so a pre-existing console error is never sold to you as chaos-discovered.
How a finding becomes a bug depends on its category, and this is where "chaos testing" gets more careful than its reputation. An action-caused page-unavailable or redirect-loop failure goes through a clean-confirmation replay: Qalizer reopens the target in a fresh browser context and replays only safe actions — navigation and readiness checks, no destructive clicks — before filing anything. If the failure doesn't reproduce without the horde, it stays unconfirmed instead of being filed. Categories that cannot be caused by a random click — a same-origin 401/403, a 5xx, a network error, a genuine console error — are recorded as evidence with the phase they were seen in. Confirmed failures and baseline issues are recorded on the monitor's Bugs page.
Two consequences worth stating plainly: an unconfirmed finding is not proof of a bug, and a confirmed finding is not proof of the latest commit's bug — the replay only establishes that it reproduces without the horde. Read the confirmation field before you write a ticket.
Separately, a saved gremlins configuration can be promoted into a scenario entry on the Scenarios page so it stays organised alongside your other tests. Promotion moves the configuration; it does not turn a finding into a scenario for you.
A screenshot is captured at the end of a run, but treat it as a smoke alarm, not an exhibit: it is the page's final state, not necessarily the state at the moment of failure. The console, network and confirmation evidence is what you should be reading.
When you get a batch of findings back, read them by clustering, not individually. One random crash is noise — maybe the monkey found a genuinely weird path, maybe it hit a flake. Ten confirmed failures reporting the same console error, or all clustering on the same page, is a signal. That cluster is your real bug. That's the one that would have hit a real user eventually, just in a slightly different costume.
Prioritize clusters by severity and reach. A cluster on your checkout page is more urgent than one on a rarely-visited settings tab. A crash that leaves the page unresponsive is worse than one that just logs an error the UI survives. And compare runs at the same action budget: a 200-action server run and a 5,000-action local run are not the same experiment, so a finding that only appears in the second one is telling you about a longer storm, not necessarily about a worse bug.
Why you shouldn't skip chaos testing
I'll be direct: chaos testing will make your app look worse before it makes it better. That's a feature, not a bug. A gremlins run on an app that has never been chaos-tested usually surfaces a pile of issues — lightly-broken error handling, missing null checks, race conditions, leaked handlers. It's uncomfortable. It's also extremely valuable, because it's the cheapest time to find them. There's no customer, no incident, no panic — just a finding and a fix.
The teams that get the most out of chaos testing run it repeatedly, alongside their scripted regression: after a risky refactor, before a big release, whenever a component has changed enough that you don't fully trust it. The scripts verify the happy path hasn't broken. The gremlins probe the unhappy path — the one your users end up on anyway. Together they give you both sides of the story: "it works as intended" and "it survives reality."
One honest note on cadence: in Qalizer today a gremlins run is started by hand. There is no scheduler integration for it and no CI trigger, so "regularly" means putting it on your release checklist rather than on a timer.
On cost: a run is charged a flat rate at start, not per action, so raising the budget on your own runner does not change the price of the run. The real cost of a bigger storm is the triage afterwards — which is exactly why you start small.
I built gremlins mode into Qalizer because I wanted that reality-probe available to a solo developer, not just teams with dedicated chaos-engineering budgets. Pick your duration and action budget, point it at a staging environment, fire, and fix what it finds — before a user finds it first.
If you want to see what a horde does to your UI, create an account (100 signup credits are added when it is created) and let the gremlins loose on a copy of your app — it might change how you think about "covered."
