
The performance difference between a buttery-smooth multiplayer server and one plagued by stuttering, frame drops, and player complaints often boils down to a single fundamental concept: thread management.
Whether you're developing for FiveM (GTA V) or RedM (Red Dead Redemption 2), both built on the CitizenFX framework, understanding how to properly manage script execution is not optional—it's the foundation of professional development.
This comprehensive guide will take you from basic concepts to advanced optimization techniques, complete with real-world examples, performance benchmarks, and battle-tested patterns used by top-tier roleplay servers.
Before we can optimize, we need to demolish a common misconception: FiveM/RedM "threads" are not operating system threads.
Both FiveM and RedM are built on the CitizenFX framework, which runs all game logic, rendering, and script execution on a single unified CPU thread—commonly called the Main Thread or Game Thread.
This single thread operates under strict time constraints:
The Critical Equation: If your scripts consume 10ms in a single frame, you've used 60% of the frame budget at 60 FPS, leaving barely enough time for the game engine itself. This immediately manifests as visible stuttering and frame drops for every player.
When you call Citizen.CreateThread(function() ... end), you're not creating an OS thread. You're creating a Lua coroutine (also called a "fiber"):
-- This creates a coroutine, NOT an OS thread
Citizen.CreateThread(function()
while true do
-- Your code here
Citizen.Wait(100)
end
end)Key Characteristics of Coroutines:
Think of it like a single chef (the CPU thread) preparing multiple dishes (coroutines). The chef can only work on one dish at a time, but they can pause one dish, work on another, then return to the first exactly where they left off.
Citizen.Wait()The Citizen.Wait() function (or its shorthand Wait()) is the single most important performance tool in your arsenal. It's not just a delay function—it's the mechanism that enables cooperative multitasking.
Here's what happens when your script runs:
Citizen.CreateThread(function()
while true do
-- 1. Scheduler resumes this coroutine
local player = PlayerPedId()
local coords = GetEntityCoords(player)
DoSomeWork(coords)
-- 2. Script calls Citizen.Wait()
Citizen.Wait(100)
-- 3. Coroutine yields control back to scheduler
-- 4. Scheduler runs other scripts and game logic
-- 5. After 100ms, scheduler resumes this coroutine
end
end)Step-by-Step Breakdown:
Wait() to the nextCitizen.Wait(x) yields control to the schedulerx millisecondsx milliseconds pass, scheduler queues your coroutine for resumptionThis is the #1 performance killer in FiveM/RedM development:
-- 🚫 CRITICAL ERROR - DO NOT USE
Citizen.CreateThread(function()
while true do
local result = ExpensiveFunction()
ProcessResult(result)
-- MISSING: Citizen.Wait()
end
end)What happens:
ExpensiveFunction()A loop without Wait() will crash the client's game, requiring a force-quit. The resource monitor will show values exceeding 100ms—completely unacceptable for any production server.
Wait(0) vs Higher ValuesWait(0) pauses the coroutine until the next game tick, while the time between each tick is frame-dependent. At 60 FPS, ticks occur every 16.6ms; at 144 FPS, every 6.9ms.
-- Wait(0) - Runs EVERY frame
Citizen.CreateThread(function()
while true do
-- This executes 60 times/second at 60 FPS
-- This executes 144 times/second at 144 FPS
Wait(0)
end
end)
-- Wait(100) - Runs ~10 times per second
Citizen.CreateThread(function()
while true do
-- Executes approximately 10 times per second
-- Frame rate independent
Wait(100)
end
end)Critical Rule: Only use Wait(0) for logic that must run per-frame, such as natives containing "ThisFrame" in their name.
Selecting the right wait time directly impacts both CPU usage and gameplay responsiveness. Always use the longest wait time that doesn't compromise functionality.
| Wait Time | Frequency | CPU Impact | Use Cases | Examples |
|---|---|---|---|---|
| 0ms | Every frame | ⚠️ VERY HIGH | Per-frame requirements only | DisableControlAction(), DrawMarker(), custom HUD elements |
| 1-10ms | 100-1000/sec | 🔴 HIGH | Critical responsiveness | Weapon recoil, vehicle handling tweaks, smooth animations |
| 50-100ms | 10-20/sec | 🟡 MEDIUM | Active gameplay checks | Proximity detection, interaction prompts, active UI updates |
| 250-500ms | 2-4/sec | 🟢 LOW | Passive monitoring | Player stats (health/armor), job status, simple zone checks |
| 1000-5000ms | 0.2-1/sec | ✅ VERY LOW | Background tasks | Data sync, cleanup routines, periodic server updates |
A marker system checking distance every frame can consume 0.4ms without any optimization—completely unacceptable for production.
-- 🚫 BAD: 0.43ms CPU usage
Citizen.CreateThread(function()
while true do
Wait(0) -- Every frame!
local player = PlayerPedId()
local coords = GetEntityCoords(player)
for _, marker in pairs(Config.Markers) do
local distance = GetDistanceBetweenCoords(
coords.x, coords.y, coords.z,
marker.x, marker.y, marker.z, true
)
if distance < 50.0 then
DrawMarker(1, marker.x, marker.y, marker.z, ...)
end
end
end
end)
-- ✅ GOOD: 0.06ms CPU usage (85% improvement!)
local nearbyMarkers = {}
-- Slow thread: Update nearby markers every 500ms
Citizen.CreateThread(function()
while true do
Wait(500) -- Only check twice per second
local player = PlayerPedId()
local coords = GetEntityCoords(player)
nearbyMarkers = {}
for _, marker in pairs(Config.Markers) do
local distance = #(coords - marker.coords) -- Use vector math!
if distance < 50.0 then
table.insert(nearbyMarkers, marker)
end
end
end
end)
-- Fast thread: Only draw markers already filtered
Citizen.CreateThread(function()
while true do
Wait(0) -- Drawing requires per-frame execution
if #nearbyMarkers > 0 then
local player = PlayerPedId()
local coords = GetEntityCoords(player)
for _, marker in pairs(nearbyMarkers) do
DrawMarker(1, marker.x, marker.y, marker.z, ...)
end
end
end
end)This optimization reduced CPU usage from 0.43ms to 0.06ms—an 85% improvement just by splitting logic into two threads with different wait times.
One of the most powerful optimization techniques is adaptive waiting—adjusting wait times based on current state.
-- ✅ EXCELLENT: Adaptive wait times
Citizen.CreateThread(function()
while true do
local player = PlayerPedId()
if IsPedInAnyVehicle(player, false) then
-- Active state: Check frequently
local vehicle = GetVehiclePedIsIn(player, false)
local speed = GetEntitySpeed(vehicle)
UpdateSpeedometer(speed)
Wait(10) -- 100 checks/second while driving
else
-- Inactive state: Minimal checking
Wait(1000) -- 1 check/second while on foot
end
end
end)Performance Impact: When the player is on foot (95% of the time for most players), this script uses 99% less CPU than a fixed Wait(10).
-- ✅ ADVANCED: Multi-state optimization
local currentState = "idle"
local stateWaitTimes = {
idle = 1000, -- Almost no activity
walking = 500, -- Moderate activity
driving = 50, -- High activity
in_combat = 10 -- Critical activity
}
Citizen.CreateThread(function()
while true do
local player = PlayerPedId()
-- Determine current state
if IsPedInAnyVehicle(player, false) then
currentState = "driving"
HandleDrivingState(player)
elseif IsPedShooting(player) or IsPedInCombat(player, 0) then
currentState = "in_combat"
HandleCombatState(player)
elseif IsPedRunning(player) or IsPedSprinting(player) then
currentState = "walking"
HandleWalkingState(player)
else
currentState = "idle"
end
-- Use state-appropriate wait time
Wait(stateWaitTimes[currentState])
end
end)Avoid monolithic scripts. Instead, organize code into specialized threads based on update frequency.
-- Thread 1: Critical per-frame operations
Citizen.CreateThread(function()
while true do
Wait(0)
-- Only per-frame natives here
DisableControlAction(0, 37, true) -- Disable weapon wheel
if showingCustomUI then
DrawCustomHUD()
end
end
end)
-- Thread 2: High-frequency gameplay checks
Citizen.CreateThread(function()
while true do
Wait(50)
local player = PlayerPedId()
if IsPedInAnyVehicle(player, false) then
CheckVehicleInteractions(player)
end
end
end)
-- Thread 3: Medium-frequency state monitoring
Citizen.CreateThread(function()
while true do
Wait(500)
UpdatePlayerStats()
CheckProximityZones()
end
end)
-- Thread 4: Low-frequency background tasks
Citizen.CreateThread(function()
while true do
Wait(5000)
SyncInventoryToServer()
CleanupOldEntities()
end
end)| Thread Type | Wait Time | Purpose | Examples |
|---|---|---|---|
| Display | 0ms | Rendering & Per-frame | HUD, markers, disabling controls |
| Input | 5-10ms | User interactions | Key detection, menu navigation |
| Gameplay | 50-100ms | Active mechanics | Proximity checks, vehicle state |
| Status | 250-500ms | Passive monitoring | Health, stats, simple zones |
| Background | 1000-5000ms | Maintenance | Data sync, cleanup, logging |
Native functions are calls to the game engine—they're expensive. Optimizing native usage can yield dramatic performance improvements.
Many native functions return values that don't change frequently:
-- 🚫 BAD: Calling PlayerPedId() every iteration
Citizen.CreateThread(function()
while true do
Wait(100)
local health = GetEntityHealth(PlayerPedId())
local coords = GetEntityCoords(PlayerPedId())
local heading = GetEntityHeading(PlayerPedId())
-- PlayerPedId() called 3 times per iteration!
end
end)
-- ✅ GOOD: Cache the player ped
Citizen.CreateThread(function()
while true do
Wait(100)
local player = PlayerPedId() -- Called once
local health = GetEntityHealth(player)
local coords = GetEntityCoords(player)
local heading = GetEntityHeading(player)
end
end)One ESX script called PlayerPedId() 18 times, when the same data could be retrieved with a single call.
FiveM provides utilities for faster distance checks—using vector subtraction (#(a-b)) instead of GetDistanceBetweenCoords() improved performance by 0.15ms.
-- 🚫 SLOW: Native function call
local distance = GetDistanceBetweenCoords(
x1, y1, z1,
x2, y2, z2,
true
)
-- ✅ FAST: Vector math (built-in Lua operator)
local distance = #(vec1 - vec2)
-- Breakdown:
-- vec1 = vector3(x1, y1, z1)
-- vec2 = vector3(x2, y2, z2)
-- #(vec1 - vec2) calculates Euclidean distance-- 🚫 TERRIBLE: Calling native every iteration
for _, player in ipairs(GetActivePlayers()) do
local targetPed = GetPlayerPed(player)
if GetDistanceBetweenCoords(
GetEntityCoords(PlayerPedId()), -- Called every iteration!
GetEntityCoords(targetPed),
true
) < 50.0 then
-- Do something
end
end
-- ✅ EXCELLENT: Cache values outside loop
local myPed = PlayerPedId()
local myCoords = GetEntityCoords(myPed)
for _, player in ipairs(GetActivePlayers()) do
local targetPed = GetPlayerPed(player)
local targetCoords = GetEntityCoords(targetPed)
local distance = #(myCoords - targetCoords)
if distance < 50.0 then
-- Do something
end
endUnderstanding your script's actual performance requires measurement. The resmon command opens the resource monitor, which displays CPU usage and memory consumption for each resource.
resmon 1 and press Enter| Column | Meaning | Target | Red Flag |
|---|---|---|---|
| Resource Name | Script identifier | - | - |
| ms (CPU Time) | Avg milliseconds consumed per second | < 0.10ms | > 0.50ms |
| mem (Memory) | RAM usage in KB/MB | Keep low | Continuously increasing |
Performance Thresholds:
If a banking script consumed 0.18ms CPU time and was optimized down to 0.01ms, server performance improved significantly.
For detailed analysis, use the profiler command to record frames:
profiler record 500
profiler viewThis opens Chrome DevTools with a flame graph showing:
Reading the Profiler:
Resources consuming more than 6ms total can cause possible issues.
Not everything needs a continuous loop. Events are zero-cost when idle.
-- 🚫 WASTEFUL: Checking every 100ms for rare action
Citizen.CreateThread(function()
while true do
Wait(100)
if IsControlJustPressed(0, 38) then -- E key
OpenDoorMenu()
end
end
end)-- ✅ EFFICIENT: Zero cost until triggered
RegisterCommand('+opendoor', function()
OpenDoorMenu()
end, false)
RegisterKeyMapping('+opendoor', 'Open Door', 'keyboard', 'E')Performance Impact: Loop-driven checking costs CPU every iteration. Event-driven code has zero CPU cost until the event fires.
-- 🚫 BAD: Polling for vehicle entry
Citizen.CreateThread(function()
local wasInVehicle = false
while true do
Wait(100)
local inVehicle = IsPedInAnyVehicle(PlayerPedId(), false)
if inVehicle and not wasInVehicle then
OnPlayerEnteredVehicle()
elseif not inVehicle and wasInVehicle then
OnPlayerExitedVehicle()
end
wasInVehicle = inVehicle
end
end)
-- ✅ GOOD: Use baseevents resource (comes with CitizenFX)
AddEventHandler('baseevents:enteredVehicle', function(vehicle, seat, displayName)
OnPlayerEnteredVehicle(vehicle, seat)
end)
AddEventHandler('baseevents:leftVehicle', function(vehicle, seat, displayName)
OnPlayerExitedVehicle(vehicle, seat)
end)While this guide focuses primarily on client-side optimization, server scripts follow similar principles with some unique considerations.
Unlike client scripts, server-side code can utilize actual OS threads for certain operations. However, the main server thread still has performance constraints.
-- 🚫 BAD: Blocking database call
local result = MySQL.Sync.fetchAll('SELECT * FROM users WHERE id = @id', {
['@id'] = playerId
})
-- ✅ GOOD: Asynchronous database call
MySQL.Async.fetchAll('SELECT * FROM users WHERE id = @id', {
['@id'] = playerId
}, function(result)
-- Handle result asynchronously
end)Why It Matters: Blocking operations on the main thread should be avoided—use async queries instead.
-- 🚫 DISASTER: Everything in one thread
Citizen.CreateThread(function()
while true do
Wait(0) -- Or any low value
CheckPlayerHealth()
CheckPlayerInventory()
CheckNearbyPlayers()
CheckNearbyVehicles()
UpdateUI()
CheckZones()
ProcessAnimations()
-- ... 50 more checks ...
end
end)Problem: All logic runs at the same frequency, regardless of necessity.
-- 🚫 WASTEFUL: Recalculating every iteration
for i = 1, 100 do
local distance = #(GetEntityCoords(PlayerPedId()) - targetCoords)
-- GetEntityCoords called 100 times!
end-- 🚫 SLOW: Using native for simple check
if GetDistanceBetweenCoords(...) < 5.0 then
-- Do something
end
-- ✅ FAST: Use vector math
if #(coords1 - coords2) < 5.0 then
-- Do something
endUse this checklist when reviewing or creating scripts:
Citizen.Wait()resmon - all resources < 0.10msprofiler record - no frame spikesLet's optimize a real proximity-based job system:
Citizen.CreateThread(function()
while true do
Wait(0)
for _, job in pairs(Config.Jobs) do
local distance = GetDistanceBetweenCoords(
GetEntityCoords(PlayerPedId()),
job.x, job.y, job.z,
true
)
if distance < 50.0 then
DrawMarker(1, job.x, job.y, job.z, ...)
if distance < 2.0 then
DrawText3D(job.x, job.y, job.z, "[E] Start Job")
if IsControlJustPressed(0, 38) then
StartJob(job.id)
end
end
end
end
end
end)local nearbyJobs = {}
local playerInJob = nil
-- Slow thread: Filter nearby jobs every 500ms
Citizen.CreateThread(function()
while true do
Wait(500)
local player = PlayerPedId()
local coords = GetEntityCoords(player)
nearbyJobs = {}
playerInJob = nil
for _, job in pairs(Config.Jobs) do
local distance = #(coords - job.coords)
if distance < 50.0 then
table.insert(nearbyJobs, job)
if distance < 2.0 then
playerInJob = job
end
end
end
end
end)
-- Fast thread: Draw only filtered jobs
Citizen.CreateThread(function()
while true do
Wait(0)
for _, job in pairs(nearbyJobs) do
DrawMarker(1, job.coords.x, job.coords.y, job.coords.z, ...)
end
if playerInJob then
DrawText3D(
playerInJob.coords.x,
playerInJob.coords.y,
playerInJob.coords.z,
"[E] Start Job"
)
end
end
end)
-- Event-driven: Handle job start
RegisterCommand('+startjob', function()
if playerInJob then
StartJob(playerInJob.id)
end
end, false)
RegisterKeyMapping('+startjob', 'Start Job', 'keyboard', 'E')Improvements:
Beyond script optimization, server configuration plays a crucial role:
# server.cfg optimizations
# OneSync - Required for 32+ players, improves entity sync
onesync on
# Server FPS target - Lower = better performance but less responsive
sv_maxclients 48 # Don't exceed 64 without optimization
# Network optimization
sv_maxrate 65000 # bytes/s per client (~520 kbps)
sv_minrate 25000
# Script rate limiting
fx_rateLimiter 200 # Prevents excessive script callsresmon - Resource monitor (F8 console)profiler - Advanced profiling toolstrdbg - Streaming debug informationMastering thread management in FiveM/RedM isn't just an optimization technique—it's the fundamental programming paradigm of the platform. Every optimization decision you make ripples through the entire player experience.
Key Takeaways:
The difference between an amateur and professional FiveM/RedM developer isn't knowing more natives—it's understanding how to use fewer of them, less frequently, with maximum efficiency.
Your players might not consciously notice perfect performance, but they'll definitely notice when it's absent. Smooth, responsive gameplay isn't a luxury—it's the foundation of player retention and server success.
Now go forth and optimize. Your players (and their PCs) will thank you.