Why Your FiveM Server Lags: The Five Script Patterns That Kill Frametime
Most FiveM lag is not your hardware. It is five recurring script patterns, from per-frame loops to synchronous SQL, and every one of them is fixable.

You upgraded the box, capped the slots, trimmed the car pack, and the server still stutters. Players blame "the city being laggy" and you cannot point at a cause.
Open resmon. In nine cases out of ten, the cause is sitting right there: a handful of scripts burning CPU on patterns that are cheap to write and expensive to run. These are the five we see most when auditing servers, with the bad version, the fixed version, and why it matters.
Resmon numbers are per-frame milliseconds. Your frame budget at 60 FPS is 16.6 ms for everything: the game, the map, and every script. A single script idling at 0.5 ms is eating 3 percent of the entire budget doing nothing.
Pattern 1: doing work every frame that does not need it
The classic. A thread with Wait(0) runs once per frame. That is the correct tool for drawing markers or checking key presses in the exact moment they happen, and the wrong tool for everything else.
-- BAD: distance-checks 30 shop locations every single frame
CreateThread(function()
while true do
Wait(0)
local coords = GetEntityCoords(PlayerPedId())
for _, shop in pairs(Config.Shops) do
if #(coords - shop.coords) < 2.0 then
DrawMarker(2, shop.coords.x, shop.coords.y, shop.coords.z, ...)
end
end
end
end)At 60 FPS that loop executes 1,800 distance checks per second to draw a marker the player sees maybe twice a session.
-- GOOD: slow scan to find the nearest shop, fast loop only while close
CreateThread(function()
while true do
local sleep = 1000
local coords = GetEntityCoords(PlayerPedId())
for _, shop in pairs(Config.Shops) do
if #(coords - shop.coords) < 20.0 then
sleep = 0
if #(coords - shop.coords) < 2.0 then
DrawMarker(2, shop.coords.x, shop.coords.y, shop.coords.z, ...)
end
end
end
Wait(sleep)
end
end)The dynamic sleep pattern drops idle cost from constant to effectively zero, and resmon shows it: 0.15 ms idle becomes 0.00 ms.
Pattern 2: uncached natives inside loops
Native calls cross the boundary between Lua and the game engine. Each call is cheap, but not free, and inside a per-frame loop the cost multiplies.
-- BAD: PlayerPedId() resolved 5 times per iteration, every frame
while true do
Wait(0)
if IsPedInAnyVehicle(PlayerPedId(), false) then
local veh = GetVehiclePedIsIn(PlayerPedId(), false)
if GetPedInVehicleSeat(veh, -1) == PlayerPedId() then
SetVehicleEngineOn(veh, true, true, false)
end
end
end-- GOOD: resolve once per tick, reuse the locals
while true do
Wait(0)
local ped = PlayerPedId()
if IsPedInAnyVehicle(ped, false) then
local veh = GetVehiclePedIsIn(ped, false)
if GetPedInVehicleSeat(veh, -1) == ped then
SetVehicleEngineOn(veh, true, true, false)
end
end
endCache anything that cannot change mid-frame: the ped, player id, vehicle handle. The same applies server-side to repeated GetPlayerPed(source) calls inside event handlers.
Pattern 3: synchronous SQL on the main thread
Server-side now. Every query written with a synchronous API blocks the whole server thread until the database answers. Ten milliseconds of query time is ten milliseconds where nothing else on your server runs: no events, no syncing, nothing.
-- BAD: blocks the entire server while the query runs
RegisterNetEvent("bank:withdraw", function(amount)
local src = source
local result = MySQL.Sync.fetchScalar(
"SELECT balance FROM accounts WHERE id = @id", { ["@id"] = src })
if result >= amount then
MySQL.Sync.execute(
"UPDATE accounts SET balance = balance - @a WHERE id = @id",
{ ["@a"] = amount, ["@id"] = src })
end
end)-- GOOD: async, the server keeps breathing while the DB works
RegisterNetEvent("bank:withdraw", function(amount)
local src = source
local balance = MySQL.scalar.await(
"SELECT balance FROM accounts WHERE id = ?", { src })
if balance and balance >= amount then
MySQL.update.await(
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
{ amount, src })
end
end)Modern oxmysql awaits yield the coroutine instead of blocking the thread. If any resource on your server still uses MySQL.Sync, it is a prime suspect for those half-second freezes players report during peak hours.
Pattern 4: broadcasting events to everyone
TriggerClientEvent(-1, ...) sends the payload to every connected player. With 5 players that is background noise. With 200, every broadcast is 200 network packets, and clients waste CPU handling events about things happening on the other side of the map.
-- BAD: every player learns about every drug sale on the map
TriggerClientEvent("drugs:startSellAnim", -1, sellerCoords)-- GOOD: only players near the seller get the event
local players = GetActivePlayers and {} or GetPlayers()
for _, id in ipairs(GetPlayers()) do
local ped = GetPlayerPed(id)
if #(GetEntityCoords(ped) - sellerCoords) < 50.0 then
TriggerClientEvent("drugs:startSellAnim", id, sellerCoords)
end
endBetter still, for state that players need to discover when they arrive, skip events entirely and use state bags, which brings us to the last pattern.
Pattern 5: state bags written every tick
State bags replicate automatically to clients, which makes them dangerously convenient. Writing one is a sync operation. Writing one per frame is a sync storm.
-- BAD: replicates the vehicle's fuel to everyone 60 times a second
CreateThread(function()
while true do
Wait(0)
Entity(veh).state:set("fuel", GetVehicleFuelLevel(veh), true)
end
end)-- GOOD: throttled, and only written when the value meaningfully changes
CreateThread(function()
local lastFuel = -1
while DoesEntityExist(veh) do
Wait(5000)
local fuel = math.floor(GetVehicleFuelLevel(veh))
if fuel ~= lastFuel then
lastFuel = fuel
Entity(veh).state:set("fuel", fuel, true)
end
end
end)Fuel does not change meaningfully in 16 milliseconds. Neither does hunger, engine health or a job counter. Pick the slowest interval that still feels responsive, and only write on change.
Finding your offenders
The workflow that works:
- 01Open resmon on an idle client, note everything above 0.10 ms while doing nothing. Those scripts have a Pattern 1 or 2 problem.
- 02Drive across the map. Scripts that spike while moving are usually distance-check loops without dynamic sleeps.
- 03Watch server-side CPU during peak hours. Freezes that correlate with player actions like banking or shops point to Pattern 3.
- 04Profile with
profiler recordand inspect the capture in Chrome tracing. Broadcast storms show up as walls of identical event handlers.
Our Script Optimizer automates the first pass: drop a resource in, and it flags per-frame loops, uncached natives and sync SQL calls with the exact file and line, so you know what to fix before your players find out for you.
If you are also fighting long load times, that is a different problem with different fixes: see the loading screen guide. And if interior FPS is your bottleneck rather than script time, start with shells versus real MLOs.
Everything we sell is held to the standard this article describes. The Orb Phone idles at 0.00 ms with its UI closed, because a phone your players carry every minute of every session has no business costing frametime.
FAQ
What resmon number is "too high"?
Rules of thumb: above 0.10 ms idle deserves a look, above 0.30 ms idle needs fixing, and anything above 1.0 ms while active better be doing visible work. Totals matter more than single scripts: thirty scripts at 0.10 ms is 3 ms of your 16.6 ms budget gone.
Is Wait(0) always wrong?
No. It is required for anything that must run every frame: drawing, key press detection with the controls natives, camera work. The mistake is leaving heavy logic inside a Wait(0) loop when a slower interval or an event-driven design does the same job.
Do more server threads fix synchronous SQL?
No. The FiveM server runs resource logic on one main thread. A blocking query stops that thread no matter what the hardware underneath looks like. The fix is async queries, not more cores.
Can bad scripts lag other servers' clients too?
On your server, yes: client scripts run on every player's machine, so one bad per-frame loop ships lag to your whole player base. That is why auditing what you install matters as much as auditing what you write.


