ESX has gone through several versions, and the naming is genuinely confusing โ "ESX Legacy" sounds like the *old* one when it is actually the current one. This guide explains what changed, how to tell which you are running, and what upgrading involves.
The naming, cleared up
ESX Legacy is the modern, maintained version. The word "legacy" refers to the project lineage, not to it being outdated. Older releases โ the 1.1 and 1.2 era, and the various forks floating around from that time โ are what people mean by "old ESX".
So: if someone tells you to run ESX Legacy, they are telling you to run the current one. If a script says "ESX Legacy only", it will not work on a 1.1 server without edits.
How to tell which version you are running
Check es_extended/fxmanifest.lua for a version line:
version '1.x.x'If there is no fxmanifest.lua at all and you find __resource.lua instead, you are on a genuinely old build โ __resource.lua was replaced by fxmanifest.lua years ago, and its presence is the clearest single signal that the resource predates modern FiveM.
Two other quick tells:
- Old ESX ships with
mysql-async. ESX Legacy uses oxmysql. - Old ESX has
esx_inventoryhudor similar in its resource list. Legacy expects ox_inventory or another modern inventory.
The code change that breaks everything
If you only remember one difference, make it this one. Getting the ESX object changed from an event to an export.
Old ESX โ the event pattern, with a wait loop:
ESX = nil
Citizen.CreateThread(function()
while ESX == nil do
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
Citizen.Wait(0)
end
end)ESX Legacy โ a direct export:
ESX = exports['es_extended']:getSharedObject()That while ESX == nil loop with Citizen.Wait(0) is the signature of an old script. It also busy-waits every frame until ESX loads, which is a real performance cost at startup multiplied by every resource doing it.
Newer ESX Legacy builds still register the old event for compatibility, but do not rely on it โ it has been deprecated for a long time and support has been removed and re-added across releases. If a script fails with attempt to index a nil value (global 'ESX'), this is almost always why. We cover that error in detail in the attempt to index a nil value guide.
Money and accounts
The account API tightened up in Legacy. Old scripts often reach straight into table fields; Legacy expects the getters and setters:
-- reading a balance
local bank = xPlayer.getAccount('bank').money
-- changing balances
xPlayer.addAccountMoney('bank', 500)
xPlayer.removeAccountMoney('bank', 250)
-- cash specifically
local cash = xPlayer.getMoney()
xPlayer.addMoney(100)Old scripts that mutate xPlayer.accounts directly will appear to work and then silently fail to persist, because the setter is what triggers the save. If money resets on relog, this is the first thing to check. Our reference pages for xPlayer.removeAccountMoney and xPlayer.getAccount cover the correct signatures.
Database layer
Old ESX used mysql-async:
MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @identifier', {
['@identifier'] = identifier
}, function(result) end)ESX Legacy uses oxmysql:
local result = MySQL.query.await('SELECT * FROM users WHERE identifier = ?', { identifier })oxmysql supports a mysql-async compatibility layer, which is why some old scripts appear to work on Legacy. But the query syntax differs โ named @parameters versus positional ? โ and mixing the two styles across your resources gets confusing fast. If your queries are failing, the oxmysql connection guide walks through diagnosis.
Why staying on old ESX hurts
- Modern scripts will not run. Anything written in the last few years targets Legacy. You will be patching every purchase.
- The ox stack assumes Legacy. ox_inventory, ox_lib and ox_target integrations are written against it.
- Security fixes only land upstream. Old forks do not receive them.
- Nobody will help you. Post an old-ESX problem in a community channel and the first reply is "update ESX".
- The busy-wait startup cost is real. Twenty resources each spinning
Citizen.Wait(0)until ESX loads is a measurably slower boot.
Upgrading: what it actually involves
Not a one-line change, but not a rewrite either.
- Back up your database. Test that the backup restores. ESX Legacy expects a slightly different
userstable shape and will alter it. - Replace `es_extended` with the current Legacy release.
- Swap mysql-async for oxmysql, or keep the compatibility layer temporarily.
- Find every old getSharedObject pattern:
`` grep -rl "esx:getSharedObject" resources/ ``
That is your migration scope. Convert each to the export.
- Fix direct account mutations. Search for
xPlayer.accountsand route through the setters. - Replace the inventory. This is usually the biggest single piece โ see ox_inventory vs qb-inventory, which covers the same migration shape.
- Test money, jobs and inventory on a copy first. Money that does not persist is the classic post-upgrade bug.
Do it on a test server. An ESX upgrade touches player data, and "we will fix it live" is how servers lose a week of player progress.
Which should you run?
For any new server, or any server you actively maintain: ESX Legacy. It is the modern standard, it is what scripts target, and it is what receives fixes.
If you are on an old build and stable, you can defer โ but every month you wait, the gap between your server and what the ecosystem publishes gets wider, and the eventual upgrade gets bigger.
Still deciding on a framework entirely?
If you are weighing ESX against QBCore in the first place, our QBCore vs ESX comparison covers the bigger decision, and what is QBCore explains the other side. Both are solid modern choices โ the important thing on the ESX side is running Legacy rather than an outdated build.
Built for ESX Legacy
At Viper Development, our scripts detect QBCore, QBox and ESX Legacy automatically, so the same resource runs on whichever you have without a separate build to keep in sync. Browse our scripts โ.