Back to Blog
Performance 13 min read January 5, 2026

FiveM Lua Performance: Writing Scripts That Don't Lag (2026)

Master Lua performance for FiveM — learn about the Citizen runtime model, common anti-patterns, optimization techniques, and how to profile your scripts for maximum efficiency.

fivem lua lua performance fivem scripting fivem optimization citizen framework

Why Lua Performance Matters in FiveM

FiveM scripts run inside a custom Lua runtime built on top of the Citizen framework. Unlike standalone Lua applications, FiveM scripts share a cooperative event loop — every resource runs on the same thread. A single poorly-written script can drag down the entire server.

In 2026, with servers scaling to 200+ simultaneous players, writing efficient Lua is no longer optional. This guide teaches you the patterns and anti-patterns that separate a well-optimized server from one plagued by desync and lag.


The FiveM Runtime Model

FiveM uses a cooperative multitasking model:

  1. Server tick loop runs at ~64 Hz (every ~15.6 ms)
  2. Client frame loop runs at the player's FPS (typically 30–120 Hz)
  3. Scripts registered via CreateThread are coroutines yielded by Citizen.Wait
  4. Between yields, no other coroutine can run — your code blocks everything

This means a single script that takes 5 ms to execute per tick steals 32% of the server's total tick budget. With 50+ scripts running, every millisecond counts.


Citizen.Wait — The Most Important Function

Citizen.Wait(ms) is how scripts yield control back to the scheduler. The parameter is the minimum time before the coroutine resumes:

Wait ValueBehaviorUse When
Wait(0)Resume next frameDrawing markers, UI, animations
Wait(100)~10 times/secondNear-player checks, interactions
Wait(500)Twice per secondMedium-range distance checks
Wait(1000)Once per secondBackground tasks, stat updates
Wait(5000+)Infrequent checksCleanup tasks, periodic syncs

The golden rule: Use the highest Wait value that still provides acceptable responsiveness for the feature.


Common Performance Anti-Patterns

1. Tick Loops with Wait(0) and No Distance Gate

-- ANTI-PATTERN: runs at 60+ FPS regardless of context

CreateThread(function()

while true do

Citizen.Wait(0)

local coords = GetEntityCoords(PlayerPedId())

for _, zone in ipairs(zones) do

if #(coords - zone.pos) < 2.0 then

DrawMarker(...)

end

end

end

end)

Problem: Even when no zone is nearby, you still call GetEntityCoords and iterate all zones every single frame.

Fix: Use a two-thread pattern — one slow thread for distance detection, one fast thread for rendering:

local nearbyZone = nil

-- Slow thread: detect nearby zone

CreateThread(function()

while true do

local coords = GetEntityCoords(PlayerPedId())

nearbyZone = nil

for _, zone in ipairs(zones) do

if #(coords - zone.pos) < 50.0 then

nearbyZone = zone

break

end

end

Citizen.Wait(nearbyZone and 200 or 1000)

end

end)

-- Fast thread: only draws when needed

CreateThread(function()

while true do

if nearbyZone then

Citizen.Wait(0)

DrawMarker(nearbyZone.type, nearbyZone.pos.x, ...)

else

Citizen.Wait(500)

end

end

end)

2. Repeated GetEntityCoords Every Frame

Getting entity coordinates is a native call that crosses the Lua-C boundary. Calling it multiple times per frame wastes cycles:

-- ANTI-PATTERN: three native calls for the same data

local dist1 = #(GetEntityCoords(ped) - pos1)

local dist2 = #(GetEntityCoords(ped) - pos2)

local dist3 = #(GetEntityCoords(ped) - pos3)

Fix: Cache it once per frame:

local coords = GetEntityCoords(ped)

local dist1 = #(coords - pos1)

local dist2 = #(coords - pos2)

local dist3 = #(coords - pos3)

3. String Concatenation in Hot Paths

Lua creates a new string object for every concatenation. In a tight loop, this causes GC pressure:

-- ANTI-PATTERN

for i = 1, 1000 do

result = result .. items[i].name .. ", "

end

Fix: Use table.concat:

local parts = {}

for i = 1, 1000 do

parts[i] = items[i].name

end

local result = table.concat(parts, ", ")

4. Table.insert in Performance-Critical Loops

table.insert(t, val) recalculates the table length on each call. For known-size tables, use indexed assignment:

-- Slower

for i = 1, count do

table.insert(results, processItem(i))

end

-- Faster

for i = 1, count do

results[i] = processItem(i)

end


Optimization Techniques

Distance Checks Before Heavy Logic

Always check if the player is close enough before running expensive code. This single technique eliminates 90% of wasted client-side computation:

local coords = GetEntityCoords(PlayerPedId())

if #(coords - targetPos) > 100.0 then

Citizen.Wait(2000) -- player is far, sleep long

goto continue

end

Caching Natives and Coordinates

Store frequently-accessed values in local variables at the top of your thread loop:

local PlayerPedId = PlayerPedId  -- cache the function reference

local GetEntityCoords = GetEntityCoords

CreateThread(function()

while true do

local ped = PlayerPedId()

local coords = GetEntityCoords(ped)

-- use coords throughout this tick

Citizen.Wait(100)

end

end)

Event-Driven vs Polling

Instead of polling for state changes every frame, use events:

-- POLLING (wasteful)

CreateThread(function()

while true do

if IsPlayerInVehicle() then doSomething() end

Citizen.Wait(100)

end

end)

-- EVENT-DRIVEN (efficient)

AddEventHandler('gameEventTriggered', function(event, data)

if event == 'CEventNetworkPlayerEnteredVehicle' then

doSomething()

end

end)

Statebags Instead of Sync Events

FiveM statebags (introduced with OneSync) allow state synchronization without custom events. They are more efficient for data that changes infrequently:

-- Setting state (server)

Player(source).state:set('onDuty', true, true)

-- Reading state (client or server)

local onDuty = LocalPlayer.state.onDuty


Profiling Your Scripts

Beyond resmon, FiveM offers a built-in profiler:

  1. Open the F8 console
  2. Type profiler record 10 — records 10 seconds of data
  3. Type profiler view — opens the profiler viewer
  4. Identify the specific functions consuming the most time

For server-side profiling, check the txAdmin Performance dashboard for per-resource CPU metrics over time.

See our complete resmon guide for detailed instructions on interpreting and acting on performance data.


Checklist: Lua Performance Review

  • ⬜ Every CreateThread loop has an appropriate Citizen.Wait value
  • ⬜ No Wait(0) without a proximity or state condition
  • ⬜ Native calls are cached at the top of loop iterations
  • ⬜ String concatenation avoids the .. operator in loops
  • ⬜ Distance checks gate all expensive logic
  • ⬜ Events are used instead of polling where possible
  • ⬜ Statebags replace custom sync events for shared state
  • ⬜ Resmon shows 0.00 ms idle for all custom scripts

Premium scripts like Jobs Creator by Alone Studios demonstrate these patterns in a production codebase — 0.00 ms idle, instant response when players interact with job markers.

→ See Jobs Creator Performance Details

Ready to Transform Your Server?

FiveM Job Creator eliminates every problem discussed in this article. 0.00ms resmon. No-code configuration. ESX & QBCore native.

Get Job Creator on Tebex — €29.99