Fixing server thread hitch warnings in FiveM

What the server thread hitch warning means, why it is a symptom rather than a cause, and how to find the code or query behind it.

Game servers10 min read

On this page
  1. Reading the message
  2. Server thread or network thread
  3. The method
  4. 1. A resource blocking the tick
  5. 2. A slow or unindexed database query
  6. 3. Database library misuse
  7. 4. A save or sync loop over every player
  8. 5. Entity count and cleanup sweeps
  9. 6. The machine underneath the server
  10. What not to do
  11. Related

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.

Main threadruns your Lua412 msWarning printedafter the stallABCABCABCACABCB holds the threadEach resource takes its turn,then gives the thread backNothing else runsNo events fireNo commands runNo other resource ticksEvery player waitsTicks resumeTime, not to scaleMain threadruns your Lua, one resource after another412 msWarningprintedABCABCACABCB holds itTime, not to scaleWhile B holds the thread, nothing else runsNo events fire, no commands run, no otherresource ticks, and every player waits.Then the warning is printedOnce the thread is back, the console reportshow long the stall lasted, 412 ms, measuredafter the fact. Then ticks resume.
A hitch on the main thread. Resources normally take their turn and give the thread back; here B holds it for 412 ms, and until it lets go no events fire, no commands run and every player waits. The warning comes afterwards, with the length of the stall. Illustration: not to scale.

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.
Fixed interval, same lengthA timer: a periodic save or a cleanup sweep.Hitch length, relativeTimeFixed interval, growingA save over every player, as more connect.Hitch length, relativeTimeWorse all session, gone on restartSomething piling up, such as entities.Hitch length, relativeRestartTimeConstant, at every sizeA loop that is not yielding.Hitch length, relativeTime
Four shapes a run of hitches can take, each pointing at a different cause below. Same length at a fixed interval is a fixed piece of work; growing at a fixed interval is a save over every player; worse all session and gone on restart is something piling up; constant is a loop that never yields. Illustration: relative, not measured.

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.

WarningThreadUsual cause
server thread hitch warningThe main thread, where your Lua runsScript code, or something it is waiting on
network thread hitch warningThe network thread, which moves packets and syncEntity 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.

  1. 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:

    Shell
    journalctl -u your-fivem-unit -f

    Then keep the console open, or keep the log, through a bad period.

  2. 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.
  3. Record a profile while it is happening

    In the server console:

    profiler record 300
    profiler save profile.json

    Open 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. Run profiler on its own to confirm the subcommands your artifact build supports. Full detail on reading it is in Optimising a FiveM server.

  4. 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 resourcename

    Do 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:

Shell
grep -rn "while true do" resources/ | head -50

Open 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:

Lua
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:

Lua
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.

Every item in one goThe thread is held until the last item.every itemABA and B wait the whole time100 items per tick, Wait(0) betweenThe server gets a turn after every slice.100AB100AB100AB100AB100Your loop: the same total in both rowsA and B: other resources on the main thread
The same loop two ways. In one go, it holds the main thread until the last item and A and B wait; with Wait(0) after every hundred items, they get a turn after each slice. The dark blocks add up to the same work in both rows. Illustration: not to scale.

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 true

The 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:

SQL
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:

SQL
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:

SQL
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:

SQL
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 .await yields, 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 a LIMIT, 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 IN or 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_string and 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:

Lua
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:

Lua
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:

Shell
vmstat 1 10
free -m

Read 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.

procsrbmemoryswpdfreebuffcacheswapsisoiobibosystemincscpuussyidwastsi and so: moving at all means you are swapping.wa: time spent waiting on disk.st: time the machine wanted the CPU and did notget it.Sustained non-zero wa or st: the constraint is themachine, not your resources.
Where those three readings sit in vmstat's output. Ignore most of the header: si and so under swap, and wa and st in the cpu group, are the columns that say whether the machine itself is the problem. Illustration: the header only, with no values.

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.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.