The Book·Chapter 5/12·11 min

Performance: make your server fly

Every resource burns CPU on every frame. Learn to measure with resmon and to write loops that don't melt your players' FPS.

A FiveM server doesn't crash because of a single heavy resource: it crashes because of the sum of many badly written ones. Every script you add asks for its little slice of CPU on EVERY frame of the game, several times per second. When that sum blows up, the symptom isn't "resource X is slow": it's that the WHOLE server stutters, cars teleport and players feel the lag (stuttering). Optimizing isn't a luxury for big servers; it's the difference between 64 comfortable players and 20 players complaining.

The unit that matters: ms per frame

The FiveM client tries to draw many frames per second. On every frame it runs the code of all your resources. If together they take too long, there isn't enough time to finish the frame and FPS drop. That's why the key metric isn't megabytes of RAM or the size of the resource: it's the milliseconds it burns per frame. A resource that spends 0.10 ms is excellent; one that spends 5.00 ms is eating your server all by itself.

Your best friend: the resmon command

FiveM ships with a built-in resource monitor. Open the client console (F8) or type the command in chat and you'll see a live table with each resource and what it consumes. It's the number 1 tool for finding the culprit.

text
resmon      -- opens the resource monitor
resmon 1    -- detailed mode: also shows server usage

-- Columns you care about:
--   CPU msec  -> milliseconds per frame (the important one!)
--   Time (%)  -> percentage of the frame it consumes
--   Memory    -> resource RAM

Monitor commands (F8 console)

FiveM colors the time column so you don't have to guess: green means it's got plenty of headroom, yellow means you should keep an eye on it, and red means that resource is getting expensive and you need to step in.

  • Green (< 0.50 ms): perfect, don't even worry about it.
  • Yellow (0.50-1.00 ms approx.): acceptable, but review it if several stack up.
  • Red (> 1.00 ms): problem. That resource needs optimization.
  • High idle: if a resource burns CPU when NOBODY is using it (a closed menu, an empty shop), it's the clearest sign of a badly written loop.

The most revealing figure in resmon is idle consumption. A well-written resource spends almost 0 ms when nothing is happening. If a shop script reads 0.80 ms without anyone being in a shop, it's running in a loop even though it doesn't need to.

The great sin: loops without Wait

Almost every performance disaster in FiveM has the same origin: an infinite loop that runs too many times per second. In Lua you write it with CreateThread and a while true do. The problem is the Wait you put inside. Wait(0) means "run me again on the next frame", that is, dozens of times per second. If inside that loop you do heavy work, you're repeating it non-stop even when there's no need at all.

lua
-- ❌ BEFORE: draws a marker and checks the key 60+ times per second,
-- wherever you are, even if the shop is 2 km away. This goes RED.
local tienda = vector3(25.7, -1345.0, 29.5)

CreateThread(function()
  while true do
    Wait(0) -- every frame, ALWAYS
    local ped = PlayerPedId()
    local pos = GetEntityCoords(ped)
    DrawMarker(1, tienda.x, tienda.y, tienda.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false)
    if #(pos - tienda) < 1.5 then
      if IsControlJustPressed(0, 38) then -- key E
        abrirTienda()
      end
    end
  end
end)

Loop that melts FPS

The fix: dynamic Wait

The technique that separates an amateur script from a professional one is adjusting the Wait based on what's happening. The idea is simple: if the player is far from the point of interest, you don't need to draw anything or watch the keyboard, so sleep the loop for a good while (say 1000 ms). Only when the player is close do you drop to Wait(0) so the marker and the key respond instantly. The result: the resource spends almost all its time idle and consumes practically 0 ms until it's really needed.

lua
-- ✅ AFTER: same behavior, but idle it burns almost 0 ms.
local tienda = vector3(25.7, -1345.0, 29.5)

CreateThread(function()
  while true do
    local sleep = 1000 -- by default, sleep 1 second
    local pos = GetEntityCoords(PlayerPedId())
    local dist = #(pos - tienda)

    if dist < 20.0 then
      sleep = 0 -- close: we respond every frame
      DrawMarker(1, tienda.x, tienda.y, tienda.z - 1.0, 0,0,0, 0,0,0, 1.0,1.0,1.0, 0,150,255,100, false,false,2,nil,nil,false)
      if dist < 1.5 and IsControlJustPressed(0, 38) then
        abrirTienda()
      end
    end

    Wait(sleep)
  end
end)

Dynamic Wait by distance

Notice the detail: when the player is 2 km away, the loop only computes ONE distance per second and goes back to sleep. That's negligible work. Only within the 20-meter radius does it drop to 0 ms so the marker looks smooth. A single script can go from 1.20 ms (red) to 0.01 ms (green) with this one change.

Cache the expensive natives

Native functions like PlayerPedId() or PlayerId() aren't free. Calling them once isn't noticeable; calling them five times per iteration inside a Wait(0) loop is. Pull them out of the tight loop and store the result in a variable. The player's ped doesn't change between lines: there's no reason to ask for it over and over.

lua
-- ❌ asks for the ped and coords several times per frame
CreateThread(function()
  while true do
    Wait(0)
    if GetEntityHealth(PlayerPedId()) < 50 then avisar() end
    if IsPedSwimming(PlayerPedId()) then nadar() end
    local c = GetEntityCoords(PlayerPedId())
  end
end)

-- ✅ cache the ped once per iteration
CreateThread(function()
  while true do
    Wait(500)
    local ped = PlayerPedId() -- a single call
    if GetEntityHealth(ped) < 50 then avisar() end
    if IsPedSwimming(ped) then nadar() end
    local c = GetEntityCoords(ped)
  end
end)

Cache natives outside the repeated work

Measure by distance with #(a - b)

The golden rule of client-side performance is: don't do work if the player isn't close. For that you need to measure distances, and FiveM gives you a lightning-fast way with the vector types. The #(vec1 - vec2) operator calculates the distance between two points directly, without the manual formula with square roots. Use it as the gatekeeper of your loops: distance first, and only if it passes the filter, the rest of the work.

lua
local pos = GetEntityCoords(PlayerPedId())
local punto = vector3(-1037.0, -2738.0, 20.0)

local distancia = #(pos - punto) -- distance in meters, done

if distancia < 50.0 then
  -- only here do we draw, check keys, etc.
end

Distance with vectors

Events over constant polling

A loop that keeps asking "yet? yet? yet?" all the time (polling) can almost always be replaced by something that tells you only when it happens. Instead of manually checking whether the player entered a zone, use tools that already do that work in an optimized way: ox_target or PolyZone for zones and interactions, and statebags to react to a change in an entity's state without watching it in a loop. Reusing these libraries isn't laziness: it's delegating the heavy lifting to code that's already optimized and battle-tested by thousands of servers.

  • Proximity interactions (entering a shop, opening a trunk): use ox_target instead of your own DrawMarker loop.
  • Geographic zones (safe areas, perimeters): use PolyZone, which fires onEnter/onExit for you.
  • React to a data change (a player cuffed, a vehicle locked): use statebags and AddStateBagChangeHandler.

Clean up the entities you create

If your script creates objects, props, peds or vehicles, you're responsible for deleting them. Every live entity consumes resources and, worse, orphaned entities pile up: a decoration script that creates props without keeping the handle leaves junk all over the map that nobody can clean. Always store the reference and remove it when you no longer need it, especially when the resource stops.

lua
local props = {}

local function crearProp(modelo, coords)
  local obj = CreateObject(modelo, coords.x, coords.y, coords.z, true, true, false)
  props[#props + 1] = obj -- we store the handle so we can delete it
  return obj
end

-- Mandatory cleanup when the resource stops:
AddEventHandler('onResourceStop', function(res)
  if res ~= GetCurrentResourceName() then return end
  for _, obj in ipairs(props) do
    if DoesEntityExist(obj) then DeleteEntity(obj) end
  end
end)

Create and clean up entities

The server side counts too

The server has a single thread for game logic. If you block it, you block it for ALL players at once. The classic mistake is running database queries inside a loop, or many queries in a row where one would do. With oxmysql use the async version (.await) so you don't freeze the thread while you wait for the database, and never query inside a for: batch the data into a single query.

lua
-- ❌ one query per player inside a loop: N trips to the DB
for _, id in ipairs(idsJugadores) do
  local row = MySQL.single.await('SELECT dinero FROM users WHERE id = ?', { id })
  -- ...
end

-- ✅ a single query with IN for all players
local rows = MySQL.query.await(
  'SELECT id, dinero FROM users WHERE id IN (?)',
  { idsJugadores }
)
-- and, in the table, an index on the column you filter by (id is already PRIMARY KEY)

One query instead of N (server)

Two more details on the server: make sure you have indexes on the columns you filter by (a WHERE on a column without an index forces the database to read the whole table), and don't use .await inside a tight loop: even though it doesn't block the thread, chaining hundreds of waits in a row still slows the operation down. Batch whenever you can.

Optimization checklist

  • Do all your loops use dynamic Wait (raising the Wait when there's no need to refresh)?
  • Is there any permanent Wait(0) that could be gated by distance?
  • Do you cache PlayerPedId() and coords outside the repeated work?
  • Do you filter by distance with #(a - b) before drawing or interacting?
  • Have you replaced polling with ox_target, PolyZone or statebags where it fits?
  • Do you delete the entities you create, also on onResourceStop?
  • Are your SQL queries outside the loops, .await, and do the filtered columns have an index?
  • Did you check resmon at idle and no resource stays yellow/red without activity?

Golden rule: measure with resmon BEFORE and AFTER every change. Don't optimize blindly or on intuition: open the monitor, note the resource's ms, apply an improvement and check that the number actually drops. If it didn't drop, the bottleneck was somewhere else.

Put it into practice

Got a question about this? The AI chat knows all of it and answers with code.

Ask the AI
FiveM optimization: resmon, Wait and best practices | Crxative-M