FiveM/RedM Script Performance Optimization: The Complete Guide to Thread Management
- Published on
- • 17 mins read•--- views

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.
🔬 Understanding the Execution Model: The Truth About "Threads"
Before we can optimize, we need to demolish a common misconception: FiveM/RedM "threads" are not operating system threads.
The Single Main Thread Architecture
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.
The Performance Budget
This single thread operates under strict time constraints:
- Target: Under 16.6 milliseconds (ms) per frame to maintain 60 FPS
- At 60 FPS: Each frame has exactly 16.6ms budget
- At 144 FPS: Each frame has only 6.9ms budget
- Reality: Frame budget must be shared between:
- Game engine rendering
- Physics calculations
- All active script execution
- Network synchronization
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.
Lua Coroutines: The Real "Threads"
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:
- Cooperative Multitasking: Coroutines don't run simultaneously; they take turns
- Pausable Execution: A coroutine can pause and resume exactly where it left off
- Scheduler-Managed: The game's scheduler decides when each coroutine runs
- Single Thread: All coroutines execute sequentially on the main thread
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.
🔑 The Foundation: Understanding 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.
The Execution Cycle
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:
- Resume: The scheduler resumes your coroutine
- Execute: Your code runs from the last
Wait()to the next - Yield:
Citizen.Wait(x)yields control to the scheduler - Schedule: Scheduler notes this coroutine shouldn't resume for
xmilliseconds - Continue: Scheduler moves to next waiting script or game logic
- Loop: After
xmilliseconds pass, scheduler queues your coroutine for resumption
The Catastrophic Anti-Pattern: The Infinite Loop
This 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:
- Loop executes
ExpensiveFunction() - Loop immediately starts again (same frame)
- Loop executes again (still same frame)
- Infinite loop on single frame
- Main thread locked up
- Game freezes completely
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.
Understanding Wait(0) vs Higher Values
Wait(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.
⏱️ Choosing Optimal Wait Times: The Performance Matrix
Selecting the right wait time directly impacts both CPU usage and gameplay responsiveness. Always use the longest wait time that doesn't compromise functionality.
The Wait Time Decision Matrix
| 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 |
Real-World Performance Examples
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.
🎯 Advanced Pattern: Dynamic Wait Times
One of the most powerful optimization techniques is adaptive waiting—adjusting wait times based on current state.
The Sleep/Wake Pattern
-- ✅ 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).
State Machine Optimization
-- ✅ 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)
🏗️ Script Architecture: Separation of Concerns
Avoid monolithic scripts. Instead, organize code into specialized threads based on update frequency.
Multi-Thread Architecture Pattern
-- 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 Organization Matrix
| 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 Function Optimization
Native functions are calls to the game engine—they're expensive. Optimizing native usage can yield dramatic performance improvements.
1. Cache Native Results
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.
2. Use Vector Math Over Native Functions
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
3. Avoid Repeated Native Calls in Loops
-- 🚫 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
end
📊 Profiling and Debugging: The Resource Monitor
Understanding your script's actual performance requires measurement. The resmon command opens the resource monitor, which displays CPU usage and memory consumption for each resource.
Using Resmon
- Press F8 to open console
- Type
resmon 1and press Enter - Monitor real-time performance metrics
Understanding Resmon Output
| 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:
- 0.00-0.10ms: ✅ Excellent
- 0.10-0.30ms: 🟢 Good
- 0.30-0.50ms: 🟡 Needs optimization
- 0.50-1.00ms: 🟠 Poor performance
- > 1.00ms: 🔴 Critical - immediate optimization required
If a banking script consumed 0.18ms CPU time and was optimized down to 0.01ms, server performance improved significantly.
Advanced Profiling
For detailed analysis, use the profiler command to record frames:
profiler record 500
profiler view
This opens Chrome DevTools with a flame graph showing:
- Which threads consume the most time
- Exact line numbers causing performance issues
- Frame time spikes and stuttering causes
Reading the Profiler:
- Green line = FPS graph
- Yellow line = CPU time per frame
- Spikes = Performance bottlenecks
- Hover over events = View exact file and line numbers
Resources consuming more than 6ms total can cause possible issues.
🎮 Event-Driven vs Loop-Driven Design
Not everything needs a continuous loop. Events are zero-cost when idle.
Loop-Driven (Bad for Infrequent Actions)
-- 🚫 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)
Event-Driven (Efficient)
-- ✅ 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.
Using Game Events
-- 🚫 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)
🖥️ Server-Side Considerations
While this guide focuses primarily on client-side optimization, server scripts follow similar principles with some unique considerations.
Server Threading Model
Unlike client scripts, server-side code can utilize actual OS threads for certain operations. However, the main server thread still has performance constraints.
Database Optimization
-- 🚫 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.
Server-Side Best Practices
- Batch database writes instead of individual inserts
- Use prepared statements for frequently executed queries
- Rate limit client-triggered server events
- Validate all client input - never trust client data
- Cache server-side data (job configs, localization strings)
🛡️ Anti-Patterns to Avoid
1. The Mega-Loop Anti-Pattern
-- 🚫 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.
2. The No-Cache Anti-Pattern
-- 🚫 WASTEFUL: Recalculating every iteration
for i = 1, 100 do
local distance = #(GetEntityCoords(PlayerPedId()) - targetCoords)
-- GetEntityCoords called 100 times!
end
3. The Unnecessary Native Anti-Pattern
-- 🚫 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
end
✅ Performance Optimization Checklist
Use this checklist when reviewing or creating scripts:
Thread Management
- All loops contain
Citizen.Wait() - Wait times are as high as possible without breaking functionality
- Threads separated by update frequency
- Dynamic wait times implemented where beneficial
- Event-driven design used for infrequent actions
Native Optimization
- Frequently called natives cached in variables
- Vector math used instead of distance natives
- Native calls moved outside loops where possible
- PlayerPedId() called once per iteration maximum
Resource Management
- Tested with
resmon- all resources < 0.10ms - Profiled with
profiler record- no frame spikes - Memory usage stable (no leaks)
- Database queries are asynchronous
- Entity cleanup implemented
Code Quality
- Comments explain optimization reasoning
- Consistent naming conventions
- Modular, reusable functions
- Error handling implemented
- Debug code removed from production
🎓 Real-World Case Study
Let's optimize a real proximity-based job system:
Before: 2.3ms CPU Usage
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)
After: 0.04ms CPU Usage (98% Improvement!)
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:
- Split into three components (filter thread, draw thread, event)
- Used vector math instead of native distance function
- Cached PlayerPedId() and GetEntityCoords()
- Reduced check frequency from 60/sec to 2/sec
- Used event-driven key detection
- Result: 98% CPU reduction
🌐 Server Configuration Optimization
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 calls
📚 Additional Resources
Official Documentation
Community Resources
Performance Tools
resmon- Resource monitor (F8 console)profiler- Advanced profiling toolstrdbg- Streaming debug information
🎯 Conclusion
Mastering 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:
- Understand coroutines: They're not OS threads—they cooperate
- Use highest possible Wait() values: CPU usage is directly proportional to check frequency
- Separate concerns: Different threads for different frequencies
- Cache aggressively: Native calls are expensive
- Use vector math: Faster than distance natives
- Profile religiously: You can't optimize what you don't measure
- Think event-driven: Zero cost when idle
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.
Join Our Community!
Get help, share ideas, get free scripts, and connect with other RedM enthusiasts in our Discord server.
Join Discord