FiveM Docs
Guides

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

FunctionSidePurpose
ESX.RegisterServerCallback(name, handler)ServerAnswer requests from clients
ESX.TriggerServerCallback(name, cb, ...)ClientAsk the server, get the answer in cb
ESX.AwaitServerCallback(name, ...)ClientAsk the server and wait for the return values
ESX.DoesServerCallbackExist(name)ServerCheck whether a name is registered
ESX.RegisterClientCallback(name, handler)ClientAnswer requests from the server
ESX.TriggerClientCallback(playerId, name, cb, ...)ServerAsk one client, get the answer in cb
ESX.AwaitClientCallback(playerId, name, ...)ServerAsk one client and wait for the return values
ESX.DoesClientCallbackExist(name)ClientCheck 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.

fxmanifest.lua
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.

server/main.lua
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):

client/main.lua
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):

client/main.lua
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.

server/main.lua
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)
client/main.lua
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.

server/main.lua
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.

client/main.lua
ESX.RegisterClientCallback('myscript:getStreetName', function(cb)
    local coords = GetEntityCoords(PlayerPedId())
    local streetHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
    cb(GetStreetNameFromHashKey(streetHash))
end)
server/main.lua
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:

  1. Use source to identify the player. Never accept a player id, identifier or job from the client arguments.
  2. Check types and ranges (type(amount) == 'number', amount > 0, amount <= limit).
  3. Re-check permissions and state on the server (job, grade, distance to a shop, item count).
  4. Do the money or item change on the server, not in the client after the callback returns.
server/main.lua
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.

ConvarDefaultEffect
xLib:callbackTimeout (or esx:callbackTimeout)300000 ms await timeoutHow 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:callbackMaxPayloadBytes65536Maximum size of the arguments a client may send to a server callback
xLib:callbackBurst / xLib:callbackRate60 / 30 per secondPer-player request budget
xLib:callbackMaxInFlight8Parallel callbacks one player may have open
server.cfg
set xLib:callbackTimeout 15000
set xLib:callbackMaxPayloadBytes 32768

When 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 return without cb(...). Every code path in the handler must call cb.
  • "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.
  • AwaitServerCallback errors outside a thread: wrap the call in CreateThread.
  • Large tables fail silently: arguments above xLib:callbackMaxPayloadBytes are 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.

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