Zum Inhalt springen
Events that stay cheap and safe

Events that stay cheap and safe

Naming events so handlers stay independent, when a function beats an event, RegisterNetEvent against AddEventHandler, payload size, latent events and the source trap.

Diese Doku ist vorerst auf Englisch.

Last updated

Events are how the two sides of FiveM talk, and how resources talk to each other without knowing anything about each other. They are also the place where a careless line becomes both a performance problem and an exploit.

For the mechanics of triggering and listening, see Client, server and events. This page is about which event to use and how to name it.

Name them resource:side:thing

Lua
TriggerServerEvent('fm_shops:server:boughtItem', item, amount)
TriggerClientEvent('fm_shops:client:stockChanged', -1, shopId, stock)

Three parts, and each one earns its place. The resource name keeps your events out of everyone else’s namespace and tells the next person where the handler lives. The side tells you which script to open. The last part says what happened.

Prefix with your resource’s real folder name. Generic names like updateStock are how two resources end up triggering each other’s handlers by accident.

Name the cause, not the effect

This is the naming rule that actually changes how code grows. An event is a fact that has happened, so name it in the past tense and describe the fact, not what you want somebody to do about it:

Lua
-- The name bakes in one handler's job
TriggerEvent('fm_chat:server:checkProfanity', message)

-- The name is the fact, and any number of handlers can care
TriggerEvent('fm_chat:server:sentMessage', src, message)

With the second name, the profanity filter is one handler, the logger is another, and the anti spam counter is a third. None of them knows the others exist and you can remove any of them without renaming anything. With the first name, the logger has to subscribe to an event called “check profanity”, which is where a codebase starts lying to its readers.

If only one thing will ever react, you probably do not want an event at all.

A local event with one handler should be a function

An event buys you one thing: several handlers, registered in places that do not know each other. If you are triggering and handling in the same resource, on the same side, with one handler, you are paying for a lookup and an argument pack to call a function you could have called directly.

Lua
-- Indirection for nothing
AddEventHandler('fm_shops:client:refresh', function() refreshUi() end)
TriggerEvent('fm_shops:client:refresh')

-- Just call it
refreshUi()

Arguments passed to TriggerEvent are packed with MessagePack and unpacked by each handler, so this is not purely a style point either.

Register only what has to cross

AddEventHandler alone does not make an event reachable from the network. RegisterNetEvent is what opens that door:

Lua
-- Server only. A player cannot trigger this.
AddEventHandler('fm_shops:server:restock', restockAll)

-- A player can trigger this, with any arguments they like.
RegisterNetEvent('fm_shops:server:boughtItem', function(item, amount) ... end)

Every RegisterNetEvent on the server is a function you have published to every player on your server, callable with whatever arguments they choose, as often as they like. Use it only when the client genuinely has to start the conversation, and treat every argument as hostile: check the type, check the range, and look the real values up server side instead of trusting the ones you were handed.

Lua
RegisterNetEvent('fm_shops:server:boughtItem', function(item, amount)
    local src = source
    if type(item) ~= 'string' then return end
    if type(amount) ~= 'number' or amount % 1 ~= 0 or amount < 1 or amount > 100 then return end

    local def = Items[item]           -- the price comes from the server, never the client
    if not def then return end
    ...
end)

GetInvokingResource is a boundary, not a lock

GetInvokingResource() returns the name of the resource that triggered the event you are handling, and returns nil when the trigger came from the other side of the network. That makes it useful for one thing: keeping other resources out of an event that is meant to be internal.

Lua
local ALLOWED <const> = { fm_core = true, fm_banking = true }

AddEventHandler('fm_shops:server:adjustStock', function(shopId, delta)
    local from = GetInvokingResource()
    if not from or not ALLOWED[from] then return end
    ...
end)

Older artifacts returned nil for an event triggered from inside the same resource as the handler, so do not build your own internal flow on top of it. If the caller is your own code, call the function.

It does not authenticate a net event. Every client trigger returns nil there, so a check like “if it is nil then it came from a trusted place” has the logic backwards. Validation on the server is the only thing protecting a net event.

Save source before you wait

On the server, source is a global that holds the player who triggered the current event. It is only guaranteed for the initial call: as soon as your handler calls Wait, or awaits a query, the global can be something else.

Lua
RegisterNetEvent('fm_shops:server:boughtItem', function(item, amount)
    local src = source            -- first line, every time
    Wait(100)
    print(src, source)            -- src is still right, source may not be
end)

Make local src = source the first line of every server event handler. It costs nothing and removes a class of bug that only shows up when two players do something at the same moment.

Ask once, with a callback

The pattern to avoid is triggering an event to ask for data and listening for a second event with the answer. It is twice the round trips, it needs two handlers, and two requests in flight at once get mixed up.

Lua
-- Ping pong
TriggerServerEvent('fm_shops:server:requestStock', shopId)
RegisterNetEvent('fm_shops:client:stockResponse', function(stock) ... end)

-- One call, with ox_lib callbacks
local stock = lib.callback.await('fm_shops:server:getStock', false, shopId)

FiveM has no callback system of its own, so this comes from a library: lib.callback in ox_lib, QBCore.Functions.TriggerCallback, ESX.TriggerServerCallback. Whichever framework you are on has one. Use it for “get me a value”, and keep plain events for “this happened”.

Payload size and how often

Two habits cover nearly all network cost:

  • Send on change, not on a timer. A HUD that receives the player’s money every frame is sending 60 times more than a HUD that receives it when it changes.
  • Send what changed, not everything. Pushing an entire inventory table because one item moved is the most common oversized payload in FiveM.

For values that change constantly and every nearby client cares about, a state bag is usually the better tool than an event: it syncs the value itself instead of a message about the value. See State bags and OneSync.

When you genuinely do have a lot of data to move, the latent variants exist for it. TriggerLatentServerEvent and TriggerLatentClientEvent take a bytes per second budget and spread the transfer out instead of blocking the network channel like a normal trigger would. The default is 25000 bytes per second when you pass -1 or 0, and the Cfx docs warn that very large values cause connectivity problems, so do not set it to ten million and call it optimisation.

Lua
TriggerLatentClientEvent('fm_shops:client:fullCatalogue', src, 50000, catalogue)

Next: Exports and resource boundaries.