Callbacks
How to use server and client callbacks in ESX Legacy, with callback and await (promise) styles, timeouts, rate limits and server-side validation.
An ESX callback is a request/response call between client and server: one side sends a request with arguments, the other side runs a handler and sends values back. Use ESX.RegisterServerCallback on the server and ESX.TriggerServerCallback or ESX.AwaitServerCallback on the client whenever the client needs data that only the server has (money, database rows, ownership checks).
Overview
| Function | Side | Purpose |
|---|---|---|
ESX.RegisterServerCallback(name, handler) | Server | Answer requests from clients |
ESX.TriggerServerCallback(name, cb, ...) | Client | Ask the server, get the answer in cb |
ESX.AwaitServerCallback(name, ...) | Client | Ask the server and wait for the return values |
ESX.DoesServerCallbackExist(name) | Server | Check whether a name is registered |
ESX.RegisterClientCallback(name, handler) | Client | Answer requests from the server |
ESX.TriggerClientCallback(playerId, name, cb, ...) | Server | Ask one client, get the answer in cb |
ESX.AwaitClientCallback(playerId, name, ...) | Server | Ask one client and wait for the return values |
ESX.DoesClientCallbackExist(name) | Client | Check whether a name is registered |
In ESX Legacy 1.16 all of these are thin wrappers around the callback system in esx_lib (xLib.callback). ESX keeps the classic handler signatures so old scripts still work:
- Server handler:
function(source, cb, ...) - Client handler:
function(cb, ...)
Create a server callback
Load the ESX object
Add the ESX import to your fxmanifest.lua so ESX is available on both sides.
fx_version 'cerulean'
game 'gta5'
lua54 'yes'
shared_script '@es_extended/imports.lua'
server_script 'server/main.lua'
client_script 'client/main.lua'Register the handler on the server
The first argument is the player's server id. Always call cb exactly once, also when you reject the request.
ESX.RegisterServerCallback('myshop:getBalance', function(source, cb, accountName)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or (accountName ~= 'money' and accountName ~= 'bank') then
return cb(nil)
end
local account = xPlayer.getAccount(accountName)
cb(account and account.money or 0)
end)Call it from the client
Callback style (non-blocking):
ESX.TriggerServerCallback('myshop:getBalance', function(balance)
if not balance then return end
ESX.ShowNotification(('Bank balance: $%s'):format(ESX.Math.GroupDigits(balance)))
end, 'bank')Await style (blocks the current thread until the answer arrives):
CreateThread(function()
local balance = ESX.AwaitServerCallback('myshop:getBalance', 'bank')
if balance then
print('Bank balance', balance)
end
end)AwaitServerCallback must run inside a thread (for example CreateThread, an event handler, or a command handler), because it suspends the coroutine while it waits.
Return several values
cb accepts any number of arguments, and they arrive in the same order on the other side.
ESX.RegisterServerCallback('myjob:getStatus', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return cb(false) end
local job = xPlayer.getJob()
cb(true, job.name, job.grade, job.onDuty)
end)CreateThread(function()
local ok, jobName, grade, onDuty = ESX.AwaitServerCallback('myjob:getStatus')
if ok then
print(jobName, grade, onDuty)
end
end)Database work inside a callback
oxmysql's .await functions fit naturally into callback handlers, because the handler already runs in its own thread.
ESX.RegisterServerCallback('mygarage:getVehicles', function(source, cb)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return cb({}) end
local rows = MySQL.query.await(
'SELECT `plate`, `vehicle`, `stored` FROM `owned_vehicles` WHERE `owner` = ?',
{ xPlayer.getIdentifier() }
)
cb(rows or {})
end)See Using oxmysql with ESX for the full query API.
Client callbacks (server asks the client)
Sometimes the server needs something only the client knows, such as the vehicle the player is looking at or a UI choice. Register the handler on the client and call it from the server.
ESX.RegisterClientCallback('myscript:getStreetName', function(cb)
local coords = GetEntityCoords(PlayerPedId())
local streetHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
cb(GetStreetNameFromHashKey(streetHash))
end)RegisterCommand('where', function(source)
if source == 0 then return end
local street = ESX.AwaitClientCallback(source, 'myscript:getStreetName')
print(('Player %s is on %s'):format(source, street))
end, false)Treat the answer from a client callback as untrusted input, exactly like a net event. A modified client can return anything.
Security: validate on the server
A server callback is a public entry point. Any connected client can call it with any arguments, as often as the rate limit allows. The rules are the same as for RegisterNetEvent handlers:
- Use
sourceto identify the player. Never accept a player id, identifier or job from the client arguments. - Check types and ranges (
type(amount) == 'number',amount > 0,amount <= limit). - Re-check permissions and state on the server (job, grade, distance to a shop, item count).
- Do the money or item change on the server, not in the client after the callback returns.
local SHOP_COORDS <const> = vector3(25.7, -1347.3, 29.5)
local PRICE <const> = 5
ESX.RegisterServerCallback('myshop:buyWater', function(source, cb, amount)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or type(amount) ~= 'number' or amount ~= math.floor(amount) or amount < 1 or amount > 10 then
return cb(false, 'invalid_request')
end
if #(GetEntityCoords(GetPlayerPed(source)) - SHOP_COORDS) > 5.0 then
return cb(false, 'too_far')
end
if not xPlayer.canCarryItem('water', amount) then
return cb(false, 'no_space')
end
if not xPlayer.removeMoney(PRICE * amount, 'Bought water') then
return cb(false, 'no_money')
end
xPlayer.addInventoryItem('water', amount)
cb(true)
end)xPlayer.removeMoney returns false when the player does not have enough money, so the check and the deduction happen in one step.
Timeouts, payload size and rate limits
The callback system in esx_lib adds protection that older ESX versions did not have. You can tune it with convars in server.cfg.
| Convar | Default | Effect |
|---|---|---|
xLib:callbackTimeout (or esx:callbackTimeout) | 300000 ms await timeout | How long a pending request waits before it is rejected. The server also cancels handlers that run too long (30 seconds unless the convar is set). |
xLib:callbackMaxPayloadBytes | 65536 | Maximum size of the arguments a client may send to a server callback |
xLib:callbackBurst / xLib:callbackRate | 60 / 30 per second | Per-player request budget |
xLib:callbackMaxInFlight | 8 | Parallel callbacks one player may have open |
set xLib:callbackTimeout 15000
set xLib:callbackMaxPayloadBytes 32768When a request is over budget or too large, the server answers with false instead of running your handler. Your client code should handle a false or nil result.
Common issues
- Callback never returns: an early
returnwithoutcb(...). Every code path in the handler must callcb. - "callback is already owned by resource": callback names are owned by the resource that registered them first. Two resources cannot register the same name. Prefix names with your resource name (
myshop:getBalance). - Callback stops working after a restart: callbacks are removed when the owning resource stops. Restart the resource that registers the callback, not only the one that calls it.
AwaitServerCallbackerrors outside a thread: wrap the call inCreateThread.- Large tables fail silently: arguments above
xLib:callbackMaxPayloadBytesare rejected. Send ids and load the data on the server instead. - Trusting the client: a callback that takes a price or an amount of money from the client is an exploit. Calculate those values on the server.
Related
- Client callback module reference
- Server functions
- xPlayer reference
- Using oxmysql with ESX
- Coding practices
Frequently asked questions
Independent documentation
Last updated on
Money and Accounts
How money works in ESX Legacy, covering cash, bank and black_money accounts, xPlayer account methods, starting money, paychecks, society money and custom accounts.
Using oxmysql
How to use oxmysql with ESX Legacy, covering query, single, scalar, insert, update, prepare and transactions with await, plus migration from mysql-async.