# Performance (/docs/guides/performance)

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 [#measure-before-you-optimize]

<Steps>
<Step>
### Client: resmon [#client-resmon]

Open the F8 console in game and run:

```txt
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.
</Step>
<Step>
### Server: profiler [#server-profiler]

In the server console (or txAdmin live console):

```txt
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](https://docs.fivem.net/docs/scripting-manual/debugging/using-profiler/).
</Step>
<Step>
### Database: slow queries [#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).
</Step>
</Steps>

## Fix thread waits [#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.

```lua title="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)
```

```lua title="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 [#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.

| State | Set by `es_extended` | Replicated to clients |
| --- | --- | --- |
| `Player(src).state.job` | On load, `xPlayer.setJob()` and job refresh | Yes |
| `Player(src).state.group` | On load and `xPlayer.setGroup()` | Yes |
| `Player(src).state.name` | On load and name change | Yes |
| `Player(src).state.identifier`, `.license` | On load | No (server only) |
| `GlobalState.playerCount` | On player load and drop | Yes |
| `GlobalState['<job>:count']` | When job counts change, for example `GlobalState['police:count']` | Yes |

Read them instead of building your own sync:

```lua title="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.

<Callout type="warn" title="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`.
</Callout>

## Use events and callbacks sparingly [#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](/docs/guides/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 [#database-performance]

### Query patterns [#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](/docs/guides/oxmysql).

### Indexes [#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:

<Steps>
<Step>
### Back up the database [#back-up-the-database]

DDL statements commit implicitly. The script is not a rollback transaction.
</Step>
<Step>
### Check for duplicates [#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.
</Step>
<Step>
### Run it in a maintenance window [#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.
</Step>
</Steps>

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 [#framework-settings-that-matter]

| Key | Default | Effect |
| --- | --- | --- |
| `Config.PaycheckInterval` | `7 * 60000` | How often paychecks run (ms) |
| `Config.PaycheckBatchSize` | `100` | Players paid before the thread yields |
| `Config.PaycheckBatchDelay` | `250` | Wait between batches (ms) |
| `Config.PaycheckJitter` | `30 * 1000` | Random delay before each run, avoids synchronized bursts |
| `Config.EnableDebug` | `false` | Extra debug output. Keep it off in production |
| `Config.AdminLogging` | `false` | Discord logging for admin commands. Each log is an HTTP request |
| `Config.LogPaycheck` | `false` | Logs every paycheck to Discord. Expensive on big servers |

All are in `es_extended/shared/config/main.lua`. See [Main config](/docs/es_extended/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 [#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 [#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.

## Related [#related]

- [Coding practices](/docs/getting-started/coding-practices)
- [OneSync functions](/docs/es_extended/server/onesync)
- [oxmysql with ESX](/docs/guides/oxmysql)
- [Database schema](/docs/guides/database-schema)
- [Troubleshooting](/docs/getting-started/troubleshooting)
