Engineering for a 0.02ms idle budget.
How rb-mechanic stays under 0.02ms idle on a 64-slot server — event batching, cache hierarchies, and the threads we never spin up.
When we set the performance budget for rb-mechanic, the number we wrote on the whiteboard was 0.02ms idle, 0.4ms peak, on a 64-slot server. That number has shaped almost every architectural decision since. Here's how we hit it.
Why idle matters more than peak
Most FiveM resources measure themselves by their peak frame cost — the spike when a player opens a UI or finishes a job. Peak is easy to win: defer the work, batch it, or just shove it onto the server.
Idle is harder. Idle is what your script costs when nothing is happening. If you have a hundred resources on the server and each idles at 0.1ms, you've burned a tenth of your frame budget on absolutely nothing. The player feels it as input lag and stutter.
The single biggest performance win we've ever shipped wasn't a faster algorithm. It was deleting a Citizen.CreateThread loop that nobody had measured.
The three rules
We landed on three rules that everything else flows from:
- No threads that aren't doing work. Every long-running thread on the client must be measurably necessary. If you spin one up for "polling," you'd better have benchmarked the event-driven version first and proven it doesn't scale.
- Cache hierarchies, not cache misses. L1: in-memory on the client. L2: server-side per-player. L3: database. Most reads end at L1. The ones that don't are pre-fetched on player join, in a single batched query.
- Events have a budget too. Every server-to-client event we fire goes through a batcher. If two events would fire in the same frame, they get coalesced. This sounds obvious until you go look at how many resources fire a
playerStateChangedevent ten times per second per player.
What the profile actually looks like
Pulled from a 48-player server, sampled over five minutes of normal RP:
[rb-mechanic]
client idle: 0.014 ms (avg)
client peak: 0.31 ms (UI open)
server tick cost: 0.08 ms / second
events / minute: 142 (87% batched)
db queries / hour: 24 (all batched joins)The peak is well under our budget. The idle is the number we're proudest of — and it's the one that took the most discipline to hold. Every release, the first thing the reviewer asks is: did this regress idle? If the answer is yes, the patch goes back, no matter how good the feature.
The threads we never spun up
The list of work we explicitly do not do on a background thread is longer than the list of work we do:
- Vehicle proximity checks. Driven by
OnEntityPlayerEnteredinstead. Free. - Job-state polling. Server pushes state changes. Client just listens.
- Heading / coordinate tracking. We sample inside the existing
EVENT_VEHICLE_MOVEDcallback. No new thread. - Phone/UI heartbeats. None. The UI lives in the NUI process and the server is the source of truth.
There's a reflex, especially in Lua, to reach for CreateThread the moment you need anything to happen "regularly." Resist it. Most of the time the thing you want is already happening — you just need to subscribe to it.
What this costs us
Honesty section: this approach is more work. The event-driven version of any feature is harder to write, harder to debug, and harder to onboard new engineers onto. We've held back at least two features this year because we couldn't figure out how to ship them inside the budget.
But this is the deal we made with the people running 64-slot servers. They don't have headroom to spare. A 0.02ms idle isn't a marketing number — it's the difference between their server feeling snappy and feeling miserable.
We'd rather ship slower than ship something that makes their players' frames worse.