Choose the right Lua scheduling pattern
Use a command or event when something happens once, SetTimeout for delayed work, and a yielding CreateThread loop for repeated work. Wait(0) yields until a following game tick; it is not a command to consume no CPU or a promise of an exact frame rate.
FiveM supplies these functions inside its Lua runtime. Running this file with a standalone Lua interpreter will not provide game natives. Start with Lua basics and a working resource manifest.
A cancellable delayed action
Create resources/[local]/doc_timer/fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'Create client.lua:
local generation = 0
RegisterCommand('docremind', function()
generation = generation + 1
local thisRequest = generation
print('Reminder scheduled. Use doccancel to cancel it.')
SetTimeout(3000, function()
if thisRequest ~= generation then return end
print('Reminder: your delayed action is ready.')
end)
end, false)
RegisterCommand('doccancel', function()
generation = generation + 1
print('Pending reminder cancelled.')
end, false)
AddEventHandler('onClientResourceStop', function(resourceName)
if resourceName ~= GetCurrentResourceName() then return end
generation = generation + 1
end)Run refresh and ensure doc_timer in the server console. In the client’s F8 console enter docremind. A message appears after approximately three seconds. Enter doccancel before it fires and no ready message should appear. Starting a second reminder supersedes the first.
The generation check makes stale callbacks harmless; it does not physically remove a scheduled callback. Do not schedule millions of callbacks or use this pattern as a persistent job queue. Resource restarts do not preserve these local variables.
Repeated work must yield
For an occasional diagnostic, use a thread with an appropriate interval. Add this to the same client script only while investigating:
CreateThread(function()
while true do
Wait(5000)
local ped = PlayerPedId()
print(('Current player ped: %s'):format(ped))
end
end)A drawing native that must render continuously usually needs a per-frame loop with Wait(0). A database query, permission check, or notification generally should not run every frame. Prefer an event or a slower interval for work that does not change with rendering.
A loop without a yield can block the runtime. Conversely, making every loop sleep several seconds can break frame-dependent drawing. Choose the interval from the feature’s correctness needs, then measure it with the profiler.
Save source before yielding on the server
The server event’s global source belongs to the event context. Capture it in a local before a wait or asynchronous callback. This separate server-side excerpt records who caused an event; it does not send delayed privileges to that ID:
-- server.lua; add server_script 'server.lua' to the manifest to run this.
AddEventHandler('playerDropped', function()
local disconnectedId = source
SetTimeout(1000, function()
print(('Disconnect observed for session ID %s'):format(disconnectedId))
end)
end)Capturing the number fixes event-context loss, not identity reuse. A player can disconnect while work is pending. Before a delayed inventory, payment, or permission mutation, verify the original session is still current and use a durable, server-verified identity for persisted records. Never trust a client-supplied target player ID.
Verify behavior under interruption
Run a reminder, cancel it, run it twice, and restart the resource before it fires. Check that stale work does not affect the next request. For server features, repeat with a player disconnect while the operation is pending. Timer delays are scheduling targets, not real-time guarantees; avoid gameplay correctness that depends on an exact millisecond callback.
Continue with resource lifecycle cleanup, events, and the official Lua runtime guide . The official event guide documents saving source across asynchronous work.