Find the resource that is eating your frames
Almost every "our server lags" thread is people guessing. There are two measurements that end the guessing in about five minutes — client resmon and the server profiler — and once you have them, the fix is usually obvious and usually small.

#First, separate the three kinds of lag
They have completely different causes and completely different fixes, and treating them as one problem is why people spend weeks on this.
| Symptom | What it actually is | Where to look |
|---|---|---|
| Low FPS, smooth network | Client-side script or streamed assets | resmon on the client |
| Good FPS, rubber-banding | Network or server tick | Server profiler, netgraph |
| Freezes at specific places | An MLO or a texture dictionary | Streaming, not scripts |
#resmon: the client-side answer
Open the client console with F8 and type resmon 1. You get a live table of every resource with its CPU time per tick and its memory. Sort by the time column and look at the top five.
The numbers are milliseconds per frame. At 60 FPS you have 16.6 ms for everything, and the game itself wants most of it.
| Per-tick time | Verdict |
|---|---|
| under 0.05 ms | Idle. Normal for a well-written resource doing nothing. |
| 0.05 – 0.20 ms | Fine. Active resource doing real work. |
| 0.20 – 0.50 ms | Worth a look, especially if it is idle-state. |
| over 0.50 ms | A problem. Something is looping every frame that should not be. |
| over 2 ms | This resource alone is costing you visible frames. |
#The loops that cause it
In practice, the overwhelming majority of client-side cost comes from one pattern: a while true loop with Wait(0) that runs whether or not it needs to. Wait(0) means every frame, forever.
-- what it usually looks like
CreateThread(function()
while true do
Wait(0)
local ped = PlayerPedId()
local coords = GetEntityCoords(ped)
for _, spot in pairs(Config.Spots) do
if #(coords - spot.xyz) < 2.0 then
DrawMarker(...)
if IsControlJustPressed(0, 38) then openMenu(spot) end
end
end
end
end)The same thing, at a fraction of the cost
Sleep long when far away, short only when close enough to matter. The behaviour is identical and the cost drops by one to two orders of magnitude.
CreateThread(function()
while true do
local sleep = 1000
local coords = GetEntityCoords(PlayerPedId())
for _, spot in pairs(Config.Spots) do
local dist = #(coords - spot.xyz)
if dist < 20.0 then
sleep = 0
DrawMarker(...)
if dist < 2.0 and IsControlJustPressed(0, 38) then
openMenu(spot)
end
end
end
Wait(sleep)
end
end)Better still: no loop at all
For fixed locations, a point or zone library removes the loop entirely — the library keeps one shared loop for every zone on the server instead of one per resource.
-- ox_lib zones: one shared loop, not one per resource
lib.zones.sphere({
coords = vec3(24.5, -1347.2, 29.5),
radius = 2.0,
onEnter = function() lib.showTextUI('[E] Shop') end,
onExit = function() lib.hideTextUI() end,
})#The server side: the profiler
The server has a real profiler built in. Record a few hundred frames while the server is busy, then read the result in a browser — it gives you a flame graph of exactly where the tick went.
- A server tick should complete in well under 5 ms. Sustained above that and players feel it as rubber-banding.
- The usual offenders are database queries inside a loop, and per-player timers that all fire on the same tick.
- A single slow synchronous MySQL call blocks the whole server, not just that player.
# in the server console
profiler record 500
profiler save
profiler view#OneSync entity culling
Under OneSync Infinity the server decides which entities each client is told about. Tuning that is one of the few settings changes that genuinely helps a busy server, because it reduces both network traffic and client-side entity count.
set onesync_distanceCullVehicles true
set onesync_forceMigration true
set onesync_workaround763185 true#Streamed assets and stutter
Freezing when you drive into a specific area is a streaming problem, not a script problem. Large MLOs, high-resolution vehicle textures and clothing packs all compete for the same budget.
- Vehicle textures are the most common waste: a 4096 × 4096 dirt map on a single car is not unusual and is entirely unnecessary. 1024 is plenty.
- Check for duplicate texture dictionaries — two resources streaming the same .ytd name is undefined behaviour and one of them will win at random.
- Raise pool sizes only when the console actually tells you a pool is full, and raise only that pool.
- Textures should be DXT compressed. An uncompressed .ytd costs several times the VRAM for no visible gain.
# only when the console names the pool
set sv_poolSizesIncrease "{ \"TxdStore\": 25000, \"CScriptEntityExtension\": 1000 }"#What to do, in order
- 01 Measure
resmon 1 on a client standing still, and profiler record 500 on the server.
- 02 Fix the top one
Not the top five. One change, then measure again, so you know what actually helped.
- 03 Look for Wait(0)
grep your resources for "Wait(0)" and read every hit. Most are wrong.
- 04 Check for duplicates
Two HUDs, two fuel scripts, two notification systems — very common on servers built from a leaked base.
- 05 Only then tune convars
Pool sizes and culling help a healthy server. They will not save one with a 4 ms script in it.
Questions
Does a custom minimap affect performance?
How many players can one server hold?
Is more CPU cores the answer?
My server is fine at 20 players and unusable at 60. Why?
Ready to pick a map?
Twelve themes on three base map styles, $8 each, instant download.