FiveM/RedM Script Performance Optimization: The Complete Guide to Thread Management

Published on
17 mins read
--- views
FiveM/RedM Script Optimization

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:

  1. Cooperative Multitasking: Coroutines don't run simultaneously; they take turns
  2. Pausable Execution: A coroutine can pause and resume exactly where it left off
  3. Scheduler-Managed: The game's scheduler decides when each coroutine runs
  4. 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:

  1. Resume: The scheduler resumes your coroutine
  2. Execute: Your code runs from the last Wait() to the next
  3. Yield: Citizen.Wait(x) yields control to the scheduler
  4. Schedule: Scheduler notes this coroutine shouldn't resume for x milliseconds
  5. Continue: Scheduler moves to next waiting script or game logic
  6. Loop: After x milliseconds 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:

  1. Loop executes ExpensiveFunction()
  2. Loop immediately starts again (same frame)
  3. Loop executes again (still same frame)
  4. Infinite loop on single frame
  5. Main thread locked up
  6. 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 TimeFrequencyCPU ImpactUse CasesExamples
0msEvery frame⚠️ VERY HIGHPer-frame requirements onlyDisableControlAction(), DrawMarker(), custom HUD elements
1-10ms100-1000/sec🔴 HIGHCritical responsivenessWeapon recoil, vehicle handling tweaks, smooth animations
50-100ms10-20/sec🟡 MEDIUMActive gameplay checksProximity detection, interaction prompts, active UI updates
250-500ms2-4/sec🟢 LOWPassive monitoringPlayer stats (health/armor), job status, simple zone checks
1000-5000ms0.2-1/secVERY LOWBackground tasksData 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 TypeWait TimePurposeExamples
Display0msRendering & Per-frameHUD, markers, disabling controls
Input5-10msUser interactionsKey detection, menu navigation
Gameplay50-100msActive mechanicsProximity checks, vehicle state
Status250-500msPassive monitoringHealth, stats, simple zones
Background1000-5000msMaintenanceData 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

  1. Press F8 to open console
  2. Type resmon 1 and press Enter
  3. Monitor real-time performance metrics

Understanding Resmon Output

ColumnMeaningTargetRed Flag
Resource NameScript identifier--
ms (CPU Time)Avg milliseconds consumed per second< 0.10ms> 0.50ms
mem (Memory)RAM usage in KB/MBKeep lowContinuously 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:

  1. Green line = FPS graph
  2. Yellow line = CPU time per frame
  3. Spikes = Performance bottlenecks
  4. 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

  1. Batch database writes instead of individual inserts
  2. Use prepared statements for frequently executed queries
  3. Rate limit client-triggered server events
  4. Validate all client input - never trust client data
  5. 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:

  1. Split into three components (filter thread, draw thread, event)
  2. Used vector math instead of native distance function
  3. Cached PlayerPedId() and GetEntityCoords()
  4. Reduced check frequency from 60/sec to 2/sec
  5. Used event-driven key detection
  6. 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 tool
  • strdbg - 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:

  1. Understand coroutines: They're not OS threads—they cooperate
  2. Use highest possible Wait() values: CPU usage is directly proportional to check frequency
  3. Separate concerns: Different threads for different frequencies
  4. Cache aggressively: Native calls are expensive
  5. Use vector math: Faster than distance natives
  6. Profile religiously: You can't optimize what you don't measure
  7. 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