FiveM Docs
Guides

Performance

Optimize an ESX Legacy server with resmon and the profiler, smarter thread waits, state bags instead of events, database indexes, paycheck batching and OneSync.

Most ESX performance problems come from scripts, not from the framework core: client loops that run every frame, server events that fire too often and database queries without indexes. This guide shows how to measure first, then fix the usual causes in order of impact.

Measure before you optimize

Client: resmon

Open the F8 console in game and run:

resmon 1

You see CPU time (ms) and memory per resource. Stand still somewhere quiet, then near the places your scripts care about (shops, job markers). A resource that stays high everywhere has an always-running loop.

Server: profiler

In the server console (or txAdmin live console):

profiler record 500
profiler view

profiler record captures the given number of frames, profiler view opens the result in the browser-based viewer. Look for resources with long tick times or event handlers that run constantly. See the cfx.re profiler docs.

Database: slow queries

oxmysql prints warnings for slow queries in the server console. Note the query and the resource. Those are your index candidates (see below).

Fix thread waits

The single biggest client cost is while true do Wait(0) ... end that runs whether or not the player is near anything.

client/main.lua
-- Bad: runs every frame for every player, everywhere
CreateThread(function()
    while true do
        Wait(0)
        local coords = GetEntityCoords(PlayerPedId())
        if #(coords - vector3(25.7, -1347.3, 29.5)) < 2.0 then
            ESX.ShowHelpNotification('Press ~INPUT_CONTEXT~ to open the shop')
        end
    end
end)
client/main.lua
-- Better: sleep long when far away, only run per frame when close
local SHOP <const> = vector3(25.7, -1347.3, 29.5)

CreateThread(function()
    while true do
        local sleep = 1000
        local distance = #(GetEntityCoords(PlayerPedId()) - SHOP)

        if distance < 2.0 then
            sleep = 0
            ESX.ShowHelpNotification('Press ~INPUT_CONTEXT~ to open the shop')
        elseif distance < 20.0 then
            sleep = 250
        end

        Wait(sleep)
    end
end)

Rules of thumb:

  • Only use Wait(0) while you draw something or read a key every frame.
  • Start job-specific loops when the player gets the job and stop them when the job changes, instead of checking ESX.PlayerData.job.name every frame. Listen to esx:setJob on the client.
  • Cache values that rarely change (PlayerId(), GetPlayerServerId(PlayerId())). Update a cached ped on esx:playerPedChanged, which es_extended triggers when the ped changes.
  • Use the built-in point helpers (ESX.Point) or ox_lib points rather than writing your own distance loops for every marker.

State bags instead of polling events

A common anti-pattern is a client that asks the server every few seconds "what is my job?" or "how many police are online?". ESX already replicates this data with state bags, which are synced by the engine only when they change.

StateSet by es_extendedReplicated to clients
Player(src).state.jobOn load, xPlayer.setJob() and job refreshYes
Player(src).state.groupOn load and xPlayer.setGroup()Yes
Player(src).state.nameOn load and name changeYes
Player(src).state.identifier, .licenseOn loadNo (server only)
GlobalState.playerCountOn player load and dropYes
GlobalState['<job>:count']When job counts change, for example GlobalState['police:count']Yes

Read them instead of building your own sync:

client/main.lua
-- Number of police online without any server callback
local function getPoliceCount()
    return GlobalState['police:count'] or 0
end

-- React to your own job changing
AddStateBagChangeHandler('job', ('player:%s'):format(GetPlayerServerId(PlayerId())), function(_, _, job)
    if job then
        print(('Job is now %s grade %s'):format(job.name, job.grade))
    end
end)

On the server, ESX.GetNumPlayers('job', 'police') returns the same count from memory without looping over players.

Do not read state bags every frame

Reading GlobalState deserializes the value. ESX itself caches the population density state in a change handler for this reason. Cache the value in a local and update it in AddStateBagChangeHandler.

Use events and callbacks sparingly

  • Do not trigger server events from a client loop. Trigger them on an action (key press, menu choice).
  • Prefer one callback that returns everything a menu needs over five callbacks.
  • ESX callbacks go through esx_lib, which rate limits callbacks per player and rejects payloads larger than xLib:callbackMaxPayloadBytes (default 65536 bytes). If your callback sends huge tables, send less data. See Callbacks.
  • On the server, ESX.GetExtendedPlayers('job', 'police') filters in memory. Use it instead of looping over GetPlayers() and calling ESX.GetPlayerFromId on every entry.

Database performance

Query patterns

  • Never run a query per player per tick. Load data once, keep it in memory, write it back on change or on a timer.
  • Select only the columns you need instead of SELECT * on users, which contains several LONGTEXT JSON columns.
  • Use MySQL.prepare for statements you run very often, and batch inserts. See oxmysql.

Indexes

legacy.sql already defines primary keys and the important secondary keys, for example owned_vehicles.owner, billing.identifier, user_licenses (owner, type) and the addon_account_data and datastore_data owner keys. Several addons add their own indexes at startup, for example esx_garage adds composite indexes on owned_vehicles and esx_banking adds idx_banking_identifier_time.

The old security_indexes.sql in ESX-Legacy-Addons is superseded and now only prints a message. Its replacement is sql/security_hardening_2026_10_02.sql. It is optional because esx_billing, esx_license, esx_addonaccount and esx_banking apply these schema changes automatically on startup. If you run it by hand:

Back up the database

DDL statements commit implicitly. The script is not a rollback transaction.

Check for duplicates

The script stops with an error if user_licenses or addon_account_data contain duplicate rows, and it never merges financial rows for you. Reconcile them first.

Run it in a maintenance window

It requires MySQL 8 or MariaDB 10.6+ and InnoDB for the economy tables. It is safe to re-run.

Do not blindly add indexes from forum posts. The file itself warns that InnoDB secondary indexes already contain the primary key, so indexes like (owner, plate) on owned_vehicles are redundant. Use EXPLAIN on the slow query first, then add the index that matches its WHERE clause.

Framework settings that matter

KeyDefaultEffect
Config.PaycheckInterval7 * 60000How often paychecks run (ms)
Config.PaycheckBatchSize100Players paid before the thread yields
Config.PaycheckBatchDelay250Wait between batches (ms)
Config.PaycheckJitter30 * 1000Random delay before each run, avoids synchronized bursts
Config.EnableDebugfalseExtra debug output. Keep it off in production
Config.AdminLoggingfalseDiscord logging for admin commands. Each log is an HTTP request
Config.LogPaycheckfalseLogs every paycheck to Discord. Expensive on big servers

All are in es_extended/shared/config/main.lua. See Main config.

Player saving is already efficient: es_extended saves all players every 10 minutes in one background thread, saves a player when they drop and saves everyone before a txAdmin scheduled restart and on shutdown. You do not need extra "save all" loops.

OneSync and the server artifact

  • ESX Legacy requires OneSync Infinity. With onesync set to off or legacy, players are rejected on join. txAdmin enables OneSync by default; in a manual setup add set onesync on.
  • es_extended requires server artifact 10188 or newer and prints an error every minute if the artifact is older. Updating the artifact is also one of the cheapest performance fixes.
  • With OneSync, prefer server-side entity creation (ESX.OneSync.SpawnVehicle, ESX.OneSync.SpawnObject) and server-side lookups (ESX.OneSync.GetPlayersInArea) over asking clients.

Common issues

  • A resource shows 0.5 ms or more in resmon everywhere: it has an always-running Wait(0) loop. Apply the sleep pattern above.
  • Server hitches every few minutes: check what runs on a timer. Paychecks, autosave and Discord logging are the usual suspects. Lower Config.PaycheckBatchSize or turn off paycheck logging.
  • Hitches when many players join: look for scripts that run heavy queries in esx:playerLoaded.
  • "ESX Requires a minimum Artifact version of 10188": update your FXServer artifact.

Frequently asked questions

Independent documentation

esx-fivem.com is a community project and is not affiliated with the ESX Framework team. Facts on this page were checked against the upstream source. Official resources: docs.esx-framework.org and github.com/esx-framework.
Edit on GitHub

Last updated on

On this page