On this page
server thread hitch warning - frame time of 412 milliseconds means the server's
main thread was blocked for 412 milliseconds. For that entire time your server
did nothing at all: no events fired, no commands ran, no resource ticked, and
every player waited. The number is how long the stall lasted, measured after the
fact.
The message is a symptom. It tells you the main thread was blocked and for how long, and it never tells you what blocked it. Something on that thread refused to give it back, and the rest of this page is how to find what.
Reading the message
The main thread is where all of your server-side script code runs, one resource after another. A stall on it stops the whole server, which is why the player report is always the same: everyone rubber-bands at the same moment, chat and commands answer late, and inventory or menu actions land seconds after the button.
Two things to take from the message itself:
- The number is the measured stall, not a threshold. The warning appears once a frame goes past a limit built into the server binary, but the figure printed is what actually happened. Compare hitches with each other. A run of 180 ms hitches and a single 4,000 ms hitch are different problems.
- A hitch of roughly the same length, repeating, is a fixed piece of work. A hitch that grows over an evening is load. That distinction narrows the list below faster than anything else you can do.
Server thread or network thread
Read the first word of the warning before anything else. server thread and
network thread are two different threads with two different sets of causes, and
treating one as the other is the most common wrong turn in FiveM troubleshooting.
| Warning | Thread | Usual cause |
|---|---|---|
server thread hitch warning | The main thread, where your Lua runs | Script code, or something it is waiting on |
network thread hitch warning | The network thread, which moves packets and sync | Entity and sync load, player volume, or hostile traffic |
If you have the network one, this is the wrong page: see network thread hitch warnings.
If both appear together, at the same times and in similar sizes, suspect neither of the above and go to the machine underneath. A stalled host stalls every thread at once.
The method
Work in this order. It converges much faster than reading code hoping to spot something.
Put a timestamp on every console line
You cannot correlate a hitch with anything until you know exactly when it happened. txAdmin's live console timestamps every line. If you run the server under systemd, the journal does it for you:
journalctl -u your-fivem-unit -fThen keep the console open, or keep the log, through a bad period.
Find the pattern
The pattern is most of the diagnosis:
- At a fixed interval, every 5, 10, or 15 minutes: a timer. Almost always a periodic save or a cleanup sweep.
- When players join or leave: join or disconnect code, usually loading or writing a character.
- Only past a certain player count: work that runs per player, so its cost multiplies.
- Constant, at every size of hitch: a loop that is not yielding.
- After a specific command or action: whatever that action calls.
Record a profile while it is happening
In the server console:
profiler record 300 profiler save profile.jsonOpen the JSON in a Chrome trace viewer at
chrome://tracing. The widest repeating block on the timeline is your cause, and it is labelled with the resource that owns it. Runprofileron its own to confirm the subcommands your artifact build supports. Full detail on reading it is in Optimising a FiveM server.Bisect if the profile is inconclusive
Stop half your non-essential resources, watch, then stop half of what is left. Each round halves the search space, so even a large server is a handful of rounds.
stop resourcenameDo it on a test server if you have one. On a live server, work from the leaves: start with resources nothing else depends on.
1. A resource blocking the tick
The most common cause, and the one to rule out first because it is cheap to check.
Symptom. Hitches that do not track player count. Often continuous, often close to the same length each time. A resource you recently added or updated.
Confirm it. The profiler names it outright. Failing that, look for loops that never yield:
grep -rn "while true do" resources/ | head -50Open each hit and check that a Wait is inside the loop body. A Wait placed
after the loop never runs. A Wait inside an if branch is a loop with no wait
for every case that does not take the branch, which is why these ship for months
before anyone notices.
Then time the suspect directly:
local started = GetGameTimer()
DoTheSuspectThing()
print(('took %d ms'):format(GetGameTimer() - started))Fix it. Give the thread back. Citizen.Wait(0) yields until the next tick,
and any positive value yields for at least that long. Work that has to touch a
large table should process a slice per tick rather than all of it in one:
CreateThread(function()
local i = 1
while i <= #items do
for _ = 1, 100 do
if not items[i] then break end
ProcessItem(items[i])
i = i + 1
end
Wait(0)
end
end)The total work is identical. The difference is that the server gets a turn every hundred items instead of once at the end.
2. A slow or unindexed database query
Symptom. It got worse as the server got popular, with no code change. Hitches line up with joins, saves, or one specific command. The table involved has grown for months.
Confirm it. Three checks, in order of effort.
Turn on your database library's own slow-query reporting. With oxmysql, in
server.cfg:
set mysql_slow_query_warning 100
set mysql_debug trueThe first prints a console warning for any query slower than 100 ms, which is
usually enough to name the query on its own. Turn mysql_debug back off
afterwards, it is noisy. Convar names move between library versions, so type each
one on its own in the server console after setting it: it prints the value
currently in force, and a name your version does not know prints nothing.
Then turn on the database server's slow log. On MySQL or MariaDB:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.2;
SHOW VARIABLES LIKE 'slow_query_log_file';SET GLOBAL applies to connections opened after you set it, so the pool your
server already holds will not report until it reconnects. Restart the server, or
set the value in the database config and restart the database, if nothing shows
up.
And catch one in flight, from another session, while the server is hitching:
SHOW FULL PROCESSLIST;A query sitting there in Sending data for seconds is your hitch, visible live.
Once you have the query, ask the database what it is doing with it:
EXPLAIN SELECT * FROM users WHERE identifier = 'char1:abc';In the result, type: ALL means a full table scan, and the rows column is
roughly how many rows it reads to answer. If that number is close to your table's
row count, there is no usable index.
Fix it. Add the index the query needs:
SHOW INDEX FROM users;
ALTER TABLE users ADD INDEX idx_identifier (identifier);Index the columns you filter and join on, not every column. Then stop asking for
data you do not use: SELECT * on a wide table with a JSON inventory column
transfers and parses the whole thing every time, even when you wanted one field.
3. Database library misuse
Separate from a slow query, because the query can be fast and the call pattern still be the problem.
Symptom. Many small queries rather than one big one. Hitches when a specific action runs. Hitches appearing across several unrelated resources at the same moment.
Confirm it and fix it. Four distinct patterns:
- Genuinely synchronous calls. The
MySQL.Sync.*family in mysql-async waits for the result before anything else on that thread runs. That is the blocking pattern by definition. Replace it with the async or awaited form. - Awaited calls are not free either. oxmysql's
.awaityields, so the server keeps running while the database works, but the result still has to be turned into a Lua table on the main thread when it arrives. A query returning tens of thousands of rows hitches on that conversion regardless of library. Add aLIMIT, or aggregate in SQL instead of in Lua. - A query per player, per item, or per loop iteration. One query inside a loop
over 60 players is 60 round trips. Fetch the set in one statement with
INor a join, then work from the table you got back. - Pool exhaustion. If every connection in the pool is busy, new queries queue
behind them, and everything awaiting a query waits. The tell is unrelated
resources hitching together with no single slow query to blame. Check the pool
size in your
mysql_connection_stringand check the database is not also serving something else.
Writes that nobody reads back, logging inserts in particular, do not need to be awaited at all. Fire them and move on.
4. A save or sync loop over every player
Symptom. A hitch at an exact interval that matches a save setting in your framework config, whose length grows through the evening as more people connect. The regularity is what identifies it.
Confirm it. Compare the interval between hitches with your framework's save
interval. Then time the save itself with GetGameTimer around it, or read the
profile: the save resource owns one enormous block and nothing else is running
inside it.
Fix it. Stagger it. Instead of saving every player on one tick, save a slice of them per cycle, so the same work is spread across many ticks:
local cursor = 1
CreateThread(function()
while true do
Wait(5000)
local players = GetPlayers()
if #players > 0 then
for _ = 1, 5 do
if cursor > #players then cursor = 1 end
SavePlayer(players[cursor])
cursor = cursor + 1
Wait(0)
end
end
end
end)Saving on meaningful events, a purchase or a job change, rather than on a clock, removes most of the need for the bulk save at all. Raising the interval is a stopgap and a poor one: it makes the hitch rarer and larger, and it widens the window of data you lose on a crash.
5. Entity count and cleanup sweeps
Symptom. Hitching gets steadily worse through a session and is gone after a restart. Or a single large hitch each time a cleanup runs.
Confirm it. With OneSync on, count what exists, at the start of a session and again hours later:
print(('vehicles %d, peds %d, objects %d'):format(
#GetAllVehicles(), #GetAllPeds(), #GetAllObjects()))A count that only climbs means nothing is being cleaned up. A single hitch at cleanup time means the sweep itself is the problem: deleting 800 vehicles inside one tick is a stall, even though deleting them is the right thing to do.
Fix it. Delete in bounded batches with a Wait between them, exactly as in
the slicing example above. Then find why they accumulate, because a cleanup sweep
is a workaround for a resource that spawns and never deletes.
Rising entity counts usually announce themselves on the network thread first, so if you are seeing both warnings, read network thread hitch warnings as well.
6. The machine underneath the server
Symptom. Both thread warnings at once. Hitches that correlate with something outside FiveM: a backup window, a cron job, another game server on the same box, a database import.
Confirm it. On Linux, while it is happening:
vmstat 1 10
free -mRead three columns. wa is time spent waiting on disk. st is time the machine
wanted the CPU and did not get it. si and so moving at all means you are
swapping, and a swapping server stalls in a way that looks exactly like bad code.
Sustained non-zero values in wa or st mean the constraint is the machine, not
your resources.
The live gauges on Server overview are the quick version of the same check.
Fix it. Move the competing job off the box or out of your peak hours, give the
database enough RAM to stop reading from disk, or stop running two servers on
resources sized for one. The
FiveM server requirements calculator
shows what a single server of your size needs. If st is high and nothing on your machine explains it,
that is a hosting question rather than a scripting one: open a ticket with the
vmstat output attached. See Opening a ticket.
What not to do
- Do not chase the warning itself. It is instrumentation. A build that printed nothing would hitch exactly as much.
- Do not restart on a timer to hide it. A scheduled restart resets the symptom of anything that accumulates, entities, memory, or a growing table, and guarantees you never find it.
- Do not upgrade the machine first. The main thread is a single thread, so more cores make no difference to a tick that takes 400 ms, and clock speed moves it by a fraction. See what actually depends on the machine.
- Do not change five things at once. Change one, restart that resource, and watch the same conditions again.
Related
resmon, the profiler, and the offenders that cause frame time in the first place.
Network thread hitch warningsThe other warning, which is not caused by the same things.
server.cfgWhere onesync, sv_maxclients, and your convars are set.
Server overviewCPU, memory, and disk on the machine itself.