A little while ago I shared how Aurora Optimizer was born and how I structured its architecture to survive Folia’s massive concurrency. Designing the system on paper (and in code) was a fun, almost textbook challenge. But if backend development teaches you anything, it’s that code compiles beautifully until it meets the real world. And on a community Minecraft server, where the budget is limited and the hardware shows no mercy, the real world hits hard.
Today I want to talk about the testing phase, the stage where you have to get pragmatic.
When I started putting the plugin through load tests, I was working from a classic and somewhat naive idea: performance must be flawless. I wanted to see the console showing an average of 19.5 TPS (ticks per second, the server’s “pulse”: 20 is the ideal) no matter how many players or entities were around.
I built a fairly complete automated environment: PowerShell scripts that started the server locally, compiled the JAR, and released a horde of bots written in Node.js (Mineflayer) to simulate stress scenarios. There was a bit of everything, from a nominal load of players moving around to “lag bombs” sustained for hours in what I called phase D (soak testing).
The initial result on my local machine? The tests failed constantly. TPS dropped, the test aborted, and I got frustrated. I wondered whether my Java code was inefficient, whether Folia’s asynchronous threads were blocking, or whether my machine simply couldn’t keep up.
Until I realized something that reframed the project: a “test failed because TPS dropped below 19.5” is the technical equivalent of your mechanic saying “the car makes a weird noise” and walking away. No context, no tools, and it hides the real nature of the problem.
The false-negative trap
In my first model, if the server took a sudden lag spike at minute 10 of a 4-hour stress test, the orchestrator aborted the run immediately. Red in the console.
What did I learn from that? Practically nothing. I was blind to the next 3 hours and 50 minutes. Did the server recover a minute later? Was it an isolated spike from Java’s garbage collector, or the start of a progressive memory leak? I’d never know, because the system killed the test before letting the plugin try to do its job.
And on a real server, stress spikes aren’t a bug: they’re part of the terrain. Players will build massive farms and trigger absurd redstone contraptions all at once. Expecting a server with limited resources to absorb that without flinching isn’t realistic. What matters isn’t avoiding the hit, but how fast you get back up after taking it.
That led me to redesign the QA strategy around a dual-gate model:
- The operational gate (blocking): the basics. The server must not crash under nominal load and must not spit out critical errors. If the server shuts down, the test fails and stops.
- The resilience gate (observational): it runs in shadow mode. If a massive spike hits and TPS collapses, the test doesn’t stop: it starts taking notes.
What gets measured now
The observational model produces telemetry instead of a binary verdict. Now I measure three things:
- Peak degradation (%): how much the hit hurt. For example: “TPS dropped 44% below normal”.
- Recovery latency: the most important stopwatch. How long does Aurora Optimizer take to detect the problem, apply its policies (pausing mob AI in a radius, reducing player synchronization), and bring TPS back to 90% of capacity? Was it 10 seconds or 5 minutes?
- Degraded time (%): over a 4-hour test, did we spend 30% of the time suffocating, or were there only intermittent drops?
This approach also solved a hardware problem. I develop and test on my local machine, which doesn’t have the multicore power of the production environment, and the old model demanded production-level performance at home, so it produced “failures” all the time. I moved those variables out of the scripts and into a declarative JSON contract: SLO profiles (service level objectives). Now the environment knows that in the local profile, holding 12 TPS under a lag bomb and recovering in under 300 seconds is a success; in production, the bar rises to 19.5. I stopped fighting the hardware and started measuring the software’s ability to adapt.
The code no longer tries to be a rigid wall that eventually cracks, but a shock absorber that gives, takes the impact, and recovers on its own. And a CSV table detailing exactly how the system behaved during a stress spike is far more useful than a plain green “Test Passed”.
In the coming weeks I’ll be consolidating the evidence from these tests to decide whether we finally give it the green light for production. Wish me luck.