Multi-threading for advanced scripts
Some jobs are too big to finish in one tick, so you split them up and do a little each tick. A tick is one frame of the game loop - the server processes everything, then moves on. If your script tries to chomp through a giant job (a database query, an HTTP call, heavy math, a huge table) all in one tick, the whole game freezes until it's done. The fix is to break the work into bites and hand control back between bites. This is an advanced move. Reach for it only after you've already tried distance checks, longer Wait intervals, and throttling, and you still need more room.
Build it
Create the resource
resources/qu_multithread_demo/
fxmanifest.lua
client.lua
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
-- Light loop: keep this fast, yields every frame
CreateThread(function()
while true do
-- light work here: UI updates, proximity checks
Wait(0)
end
end)
-- Heavy loop: chunk the work and yield WITHIN the loop
CreateThread(function()
while true do
-- Process a large table a few items at a time, then yield.
-- Without the inner Wait(0) this whole pass blocks the tick,
-- because both loops share the same single thread.
local allVehicles = GetGamePool('CVehicle')
for i, v in ipairs(allVehicles) do
-- Heavy processing for one vehicle here
if i % 20 == 0 then
Wait(0) -- yield to the scheduler between chunks
end
end
Wait(1000) -- re-run the bulk pass once per second
end
end)
Understand thread scheduling
Yielding often is the whole trick - smoothness comes from giving everyone else a turn, not from running "in parallel." All your threads take turns on one lane. Every Wait() is you stepping aside so the scheduler can run the next ready coroutine. A loop that grinds through heavy work without a single Wait() blocks EVERYTHING until it finally hits one. Two CreateThread loops still share that one lane - splitting the work only helps if each loop yields often. So:
- Light loop: yields every frame (Wait(0)), stays responsive
- Heavy loop: yields mid-pass (Wait(0) every chunk), then waits a second before the next bulk pass
The game loop, rendering, and every other resource ride that same single lane with your code. Step aside often and the frame stays smooth.
Use SetTimeout for deferred one-shot work
Sometimes you don't need a loop at all. You just want to run one heavy job, but not right now - get out of this tick's way first:
-- Defer heavy work without blocking the current tick
SetTimeout(0, function()
-- This runs on the NEXT tick, not this one
local result = ProcessHeavyData()
SendNUIMessage({type = 'dataReady', data = result})
end)
SetTimeout(ms, fn) runs your function after ms milliseconds and lets the code around it keep going in the meantime. Pass 0 and it's the FiveM version of setTimeout(fn, 0) in JavaScript: "run this the moment you can, just not inside this tick." The current tick finishes clean, then the function fires on the next available one.
Keep reading the full lesson
Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.
- Common failures
The remainder of Multi-threading for advanced scripts is available to FiveM School members.