On this page
- First, decide which side is slow
- Reading resmon
- What the numbers mean
- The memory column
- resmon 2
- Measuring the server
- txAdmin's performance chart
- The built-in profiler
- Timing one suspect by hand
- The offenders you will actually find
- A loop with no wait
- Every-frame work that does not need to be every frame
- Database queries in a tick
- Events and state bags sent to everyone
- Too many entities
- Map and MLO resources
- Console output and webhooks
- OneSync, player count, and population
- What actually depends on the machine
- A workflow that finds it
- Related
Almost every FiveM performance problem is resource code, not the machine. Before you change anything, work out which side is slow, the server or the client, because the tools, the numbers, and the fixes are different for each.
This page is the full method: how to measure, how to read what you measured, and what to do about the things you will find. If you arrived mid-incident with a warning in your console, go straight to server thread hitch warnings or network thread hitch warnings and come back here afterwards.
First, decide which side is slow
The single most expensive mistake in FiveM tuning is fixing the wrong side. These symptoms sort themselves.
| What you see | Where the problem is |
|---|---|
server thread hitch warning in the server console | The server's main thread. Your server-side script code, or a query it is waiting on. |
network thread hitch warning in the server console | Packet and sync load, not script code. Different page, different causes. |
| Everyone rubber-bands at once, commands and menus answer late, chat lags, but frame rate is fine | The server |
| Frame rate drops, stutter when you drive into a busy area, textures go flat or turn white | The client |
| One player is slow and nobody else is | That player's machine or connection |
| Everything is fine until roughly the same player count every night | Load that scales with players: sync, per-player loops, or queries per player |
Frame rate and server performance are separate measurements. A server can be hitching for half a second at a time while every client sits at a steady frame rate, and a client can be at 20 fps on a server whose main thread is idle.
Reading resmon
Press F8 in game to open the client console and type resmon. You get a live
table of every running resource with the CPU time it spent in the last frame and
the memory it holds.
What the numbers mean
The CPU column is time per frame, in milliseconds. Judge it against your frame budget rather than against a figure someone posted on a forum:
- At 60 fps a frame is 16.67 ms long.
- At 100 fps it is 10 ms.
- At 144 fps it is 6.94 ms.
Everything the game does in a frame, including all of your resources plus the game engine itself rendering the world, has to fit inside that budget. The engine takes the large majority of it. What your resources have left is a small fraction, which is why a single resource sitting at 1 ms is already taking a noticeable slice, and why several resources at 0.5 ms each add up to a frame rate you can feel.
So the useful reading is relative, and there are three questions worth asking of the table:
- Which resources are at the top? Sort by the CPU column and look at the top three. Those are the only ones worth your time. Optimising the fourth is effort you will not be able to measure.
- Does the cost change when you are not using the resource? A menu that costs while it is open and nothing while it is closed is behaving correctly. A resource that costs the same whether you touch it or not is doing work nobody asked for, every frame, forever.
- Does the cost climb as you move? Open resmon and drive from an empty part of the map into a dense one. A resource whose cost climbs with entity density is running per-entity or distance work every frame.
Watch it for a few seconds rather than reading one frame. The numbers move constantly and a single spike proves nothing.
The memory column
Memory in resmon is the resource's Lua or JS heap, not its streamed assets. What matters here is the shape over time. A number that sits still is fine. A number that only ever climbs, across a session, is a leak: a table being appended to and never cleared, or handlers being registered repeatedly.
Streamed assets, meaning everything in a resource's stream folder, do not show
up as CPU cost. They cost download time on join and video memory once loaded, and
they are the usual cause of flat or missing textures.
resmon 2
resmon 2 expands the view. Instead of one line per resource you get the
individual threads and event handlers inside each one, with their own timings. It
turns "this resource costs 1.2 ms" into "this one tick inside this resource costs
1.2 ms", which is the difference between guessing and knowing which function to
open.
Use resmon 1 to find the resource, then resmon 2 to find the code inside it.
Measuring the server
There is no resmon for the server. Two tools cover it.
txAdmin's performance chart
txAdmin ships with the server artifacts and its dashboard carries a performance chart with a selector for the server's threads. It plots how long ticks took over a window of time, so it answers when the server was slow and which thread was slow. It does not tell you which resource, but it is the fastest way to confirm that a problem is real, that it is recurring, and whether it lines up with your player count.
Two readings are worth taking before you go any further:
- Does the slow period line up with peak players? If it does, the cost scales with players, which points at per-player loops and per-player queries.
- Which thread? If the main thread is clean and the network thread is not, stop reading here and go to network thread hitch warnings.
The built-in profiler
The profiler is the tool that names the resource. It runs in the server console:
profiler record 300
profiler save profile.jsonThe first command records a number of frames, the second writes the recording to
a file in your server data directory. Load that JSON into a Chrome trace viewer
at chrome://tracing to read it, or load it on a game client with profiler load
and profiler view.
Run profiler on its own first. It prints the subcommands your artifact build
supports, and that list is the authority for your version rather than anything
written here.
Two rules for getting a useful recording:
- Record while the problem is happening. A profile of an idle server at 4am tells you nothing. Start the recording when you can see hitches in the console or a spike on the txAdmin chart.
- Record a short window. Long recordings produce files that are painful to open and no more informative.
Reading the result: each tick appears as a block on a timeline and the width of a block is the time it took. You are looking for the widest block that repeats. The resource it belongs to is named on it.
Timing one suspect by hand
When you already suspect a specific function, time it directly. This works on both the server and the client:
local started = GetGameTimer()
DoTheExpensiveThing()
print(('DoTheExpensiveThing took %d ms'):format(GetGameTimer() - started))If that print is anywhere near the size of the hitches you are seeing, you have your answer and you can stop looking.
The offenders you will actually find
A loop with no wait
The first thing to rule out, and still the most common cause of a stalled server.
Citizen.CreateThread(function()
while true do
-- nothing yields here
DoWork()
end
end)A FiveM thread only gives time back to the runtime when it yields, and
Citizen.Wait is what yields. A loop with no wait inside it never gives time
back. On the client that costs frame rate. On the server it stalls the main
thread, and while it is stalled nothing else on the server runs at all: no
events, no commands, no other resource.
Find the candidates:
grep -rn "while true do" resources/ | head -50Then open each hit and check that a Wait is inside the loop body, not after it
and not inside a branch that only sometimes executes. A Wait that sits behind an
if is a loop with no wait for every player who does not meet the condition.
Every-frame work that does not need to be every frame
Citizen.Wait(0) means "run me again next frame". Very little needs that.
Drawing natives do, because what they draw exists for exactly one frame. Distance
checks, state checks, database reads, and anything to do with inventory or money
do not.
The standard fix is a loop with a variable sleep: run slowly until you are near the thing, then run every frame only while you are near it.
local target = vector3(-1037.0, -2737.0, 20.1)
CreateThread(function()
while true do
local sleep = 1000
local coords = GetEntityCoords(PlayerPedId())
if #(coords - target) < 20.0 then
sleep = 0
DrawMarker(--[[ marker args ]])
end
Wait(sleep)
end
end)A map of 40 markers written the naive way is 40 threads running every frame. The same map written this way is 40 threads running once a second, and one or two of them running every frame while you stand next to something.
Database queries in a tick
A query inside a loop is the classic way to turn a working server into a hitching one at 30 players. Read once, keep the result in a table, and write on change rather than on a timer. Full detail, including how to prove it is your database rather than your code, is on server thread hitch warnings, because that is where this one usually announces itself.
Events and state bags sent to everyone
TriggerClientEvent(name, -1, payload) sends to every connected player. Called
once, that is nothing. Called on a timer, or once per player in a loop over all
players, the cost multiplies by your player count and lands on the network side.
Three habits that keep it small:
- Send to the players who need it, not to
-1. - For anything large, use
TriggerLatentClientEvent, which takes a byte rate and spreads the payload out over time instead of pushing it all at once. - When you set a state bag, pass the replication argument deliberately.
Entity(vehicle).state:set('key', value, true)replicates to clients, and doing that inside a tick replicates it every frame. If clients do not read the value, passfalse.
Too many entities
Every networked entity is work: it is tracked, it is synced, and it is considered for every player near it. Servers accumulate them. Vehicles nobody deleted, props from a job that crashed, peds from a script that spawned them and forgot.
With OneSync on you can count them from the server console or a command:
print(('vehicles %d, peds %d, objects %d'):format(
#GetAllVehicles(), #GetAllPeds(), #GetAllObjects()))Take that count when the server is fresh and again after a busy evening. A number that only grows is a cleanup problem, and it will show up as a network thread problem long before it shows up as a main thread one.
sv_entityLockdown in your server.cfg decides whether clients may create
entities at all. Setting it to relaxed or strict stops a modded client
spawning a thousand objects into your server, which is both a performance control
and a security one. See
server.cfg for where it goes.
Map and MLO resources
Streamed content does not cost server tick time. It costs:
- Client video memory, which is what causes flat or white textures when it runs out.
- Download time on join, once per player per change.
- Disk reads on your server while players are downloading it.
So a heavy car pack or an unoptimised interior is a client problem and a join-time problem. It is not why your server thread hitches, and moving to a bigger plan will not change it. Find the large ones:
du -sh resources/*/stream 2>/dev/null | sort -h | tail -20The usual offenders are uncompressed textures at resolutions nobody sees, and several copies of the same vehicle pack under different names.
Console output and webhooks
print is not free, and printing on every tick is slow enough to measure,
particularly on Windows where console writes are expensive. The same goes for a
Discord webhook fired per event: each one is an HTTP request, and a busy server
firing one per player action will queue them faster than it drains them.
Keep logging proportionate: log the events you would actually read.
OneSync, player count, and population
These live in your server.cfg and they set the ceiling on everything above.
onesynctakeson,legacy, oroff. With it off you are on the base GTA network model and limited to 32 clients.onis the current mode and is what raises the ceiling, because entities are culled by distance so each client only receives what is near it.legacyis the older behaviour and does not scale the same way.sv_maxclientsis the slot count you ask for. What you actually get depends on the OneSync mode and on your licence tier, and the server clamps your number to it, so read the startup console for the limit it settled on. Setting it far above what your scripts can carry does not add capacity, it only lets more people in before the hitching starts. See server.cfg.onesync_populationcontrols whether ambient traffic and pedestrians are networked. Turning it off takes a whole class of entities out of sync work. On a roleplay server where ambient traffic is decoration rather than gameplay, this is one of the few settings that changes entity counts by a large factor. Measure your entity counts before and after with the snippet above rather than trusting that it helped.
Typing a convar name on its own in the server console prints its current value, which is the reliable way to see what is actually in force rather than what you think your config sets.
What actually depends on the machine
This is the part people skip, so it is worth being blunt about.
The FiveM server's main thread is one thread. All of your server-side Lua runs on it, one resource after another. That has a direct consequence: adding cores does not make any single tick faster. A resource whose tick takes 20 ms of work still takes about 20 ms on a machine with four times the cores, because it was never able to use the second core in the first place. Single-thread clock speed does move that number, but it moves it by a fraction, not by an order of magnitude. There is no CPU that turns a 20 ms tick into a 2 ms tick. Only editing the code does that.
What the machine genuinely decides:
- Database performance. Query time is disk and memory bound. A MySQL instance with enough RAM to hold its working set in the buffer pool answers in a fraction of the time of one that reads from disk on every query. This is real and it is measurable.
- Headroom. If something else on the box, a backup job, a second game server, a web panel, takes the CPU while your server needs it, your server hitches through no fault of its own.
- Whether you swap. A server that runs out of RAM and starts swapping stalls in a way that looks exactly like bad code and is not.
- Join and download experience. Asset delivery on join is disk and bandwidth.
Before concluding it is the machine, check it, on Linux:
vmstat 1 5
free -mIn vmstat, wa is time waiting on disk and st is time your machine wanted the
CPU and did not get it. Sustained non-zero values in either column mean the
machine really is the constraint. si and so moving at all means you are
swapping. If all of those are near zero while your server hitches, the machine is
idle and waiting on your code.
A workflow that finds it
Write down the symptom in measurable terms
"Hitches of about 300 ms, every few minutes, from around 40 players" is something you can test against. "The server lags" is not.
Confirm which side and which thread
Read the server console for hitch warnings and check the txAdmin performance chart. Client-side complaints with a clean server go to resmon, server-side warnings go to the profiler.
Measure while it is happening
Record a profile, or open resmon, during the bad period. Measurements taken at a quiet time will point you at the wrong resource.
Change one thing
Fix, restart the one resource with
restart resourcename, and measure again under the same conditions. Two changes at once and you will not know which one mattered, which is also true of settings changes.Keep the measurement
Note the before and after numbers. Performance work without numbers turns into folklore, and folklore is how a server ends up with 30 settings nobody can justify.
Related
The main thread stalled. Causes in likelihood order, with the check for each.
Network thread hitch warningsPacket and sync load, which is a different problem with different fixes.
server.cfgWhere onesync, sv_maxclients, and the population settings live.
Artifacts and updatesWhich build you are on, and how to change it.