Every docs URL answers with Markdown when you add .md. Site map: llms.txt
Exports and resource boundaries
What an export call really costs in FiveM, keeping payloads small without making the API chatty, shared files that skip the boundary, and naming an API people can use.
Diese Doku ist vorerst auf Englisch.
An export looks like a function call and is not one. exports.banking:GetBalance(id) reaches into another resource, which has its own Lua state, so the arguments and the return value have to be packed on one side and unpacked on the other. The FiveM Lua scheduler does that with MessagePack, through a function reference.
None of that is slow for a call you make when a player opens a menu. It becomes the thing you measure when the call sits inside a loop, or when the payload is large.
For the basics of defining and calling exports, see Exports and dependencies.
Call it less, not smaller
The first instinct when someone hears “exports serialise” is to make the API tiny. That trades one problem for another:
-- One boundary crossing, one big payload
local data = exports.fm_core:GetPlayerData(src)
-- 200 boundary crossings
local items = {}
for i = 1, 200 do
items[i] = exports.fm_core:GetInventorySlot(src, i)
endNeither extreme is the answer. The rule that holds up is design the export around what the caller needs, so there is one call and it returns exactly that:
exports('GetInventorySlots', function(src, from, to) ... end) -- one call, the range you want
exports('GetBalance', function(src) return accounts[src] or 0 end) -- one number, no wrapper tableA GetPlayerData style export that hands back the whole player object on every call is the shape to avoid, because every caller pays for the parts it does not use, and because the caller gets a copy: changing a field on the table you received does not change anything in the owning resource. That surprises people regularly.
Inside one resource, nothing is copied
Tables are references in Lua. Returning a big table from a local function to your own code hands back a pointer, not a copy, and costs nothing. Keep that in mind before you split your own internal API into accessors “for performance”: the copy only exists at the resource boundary.
Hoist the lookup out of a loop
exports.banking goes through a metatable and caches the reference after the first use, so the second call is cheaper than the first. If you are calling in a loop anyway, take the handle out of it:
local banking = exports.fm_banking
for i = 1, #players do
banking:AddBalance(players[i], 100)
endBetter still, give the other resource a call that takes the list:
exports.fm_banking:AddBalanceBulk(players, 100)One crossing instead of n, and the owning resource can do the whole thing in one database transaction.
Shared files skip the boundary entirely
Constants, enums and pure helper functions do not need an export at all. A file loaded into your resource with @resource/file runs in your Lua state, so calling it is an ordinary Lua call with no packing:
shared_scripts {
'@ox_lib/init.lua',
'@fm_core/shared/enums.lua',
}That is why ox_lib is used this way: lib.callback, lib.points, lib.requestModel and friends are your own code once the file is loaded. Use shared files for anything with no state of its own, and keep exports for things only the owning resource can answer, like a balance or an inventory.
Remember that a shared file is downloaded by clients. Do not put prices, checks or secrets in one. See Scope, files and layout.
Name an API you would want to use
- PascalCase for export names, matching the rest of FiveM:
GetBalance,AddItem,HasLicense. - Start with a verb, and make
Getmean “reads nothing changes” andSetorAddmean “changes something”. - Annotate every export with Lua Language Server comments so the people calling it get parameter hints. See Functions that stay small.
- Return something a caller can branch on.
falseplus a reason beats an error the caller cannot catch. See Error handling.
---@param src number Player server id
---@param amount number Positive amount
---@return boolean ok
---@return string? reason 'invalid_amount' | 'insufficient_funds'
local function removeBalance(src, amount)
if type(amount) ~= 'number' or amount <= 0 then return false, 'invalid_amount' end
local balance = accounts[src] or 0
if balance < amount then return false, 'insufficient_funds' end
accounts[src] = balance - amount
return true
end
exports('RemoveBalance', removeBalance)Keeping the real function local and exporting it separately means your own resource calls it directly, with no boundary, and only other resources pay for the crossing.
Declare what you depend on
dependencies { 'ox_lib', 'oxmysql' }FXServer starts dependencies first and refuses to start your resource if one is missing, which turns a confusing No such export at 3am into a clear line in the console at startup. For an optional integration, check instead of depending:
local hasTarget = GetResourceState('ox_target') == 'started'Next: Error handling.
