FiveM events: RegisterNetEvent, TriggerServerEvent, TriggerClientEvent

How FiveM client and server events work: RegisterNetEvent, AddEventHandler, TriggerServerEvent, TriggerClientEvent with -1, the source variable and event naming.

Most script bugs between the game and the server come down to events: a handler that never runs, an event sent to nobody, or a source that points to the wrong player.

This article explains how network events work, how to register and trigger them in both directions, and how to name them so that two scripts never collide.

The two sides

A FiveM resource can have code on the client (one copy per player) and on the server (one copy). They talk through events:

From To Function
Client Server TriggerServerEvent
Server One client TriggerClientEvent(name, playerId, ...)
Server All clients TriggerClientEvent(name, -1, ...)
Same side Same side TriggerEvent

Arguments are serialised, so you can send numbers, strings, booleans and tables. You cannot send functions. Entity handles are local to each machine, so send a network id (NetworkGetNetworkIdFromEntity) instead.

Registering a handler

A handler is a function that runs when the event arrives. For an event that comes from the network you must also mark it as allowed:

lua
RegisterNetEvent('my_script:client:notify', function(message)
    print(message)
end)

That is the short form: RegisterNetEvent with the function as its second argument registers the event and attaches the handler. The long form does the same in two steps:

lua
RegisterNetEvent('my_script:client:notify')
AddEventHandler('my_script:client:notify', function(message)
    print(message)
end)

If you only use AddEventHandler, the handler works for local TriggerEvent calls, but a TriggerServerEvent or TriggerClientEvent from the other side is ignored. That is the most common reason for an event that does nothing.

Client to server

lua
-- client
TriggerServerEvent('my_script:server:buy', 'water', 2)
lua
-- server
RegisterNetEvent('my_script:server:buy', function(item, amount)
    local src = source
    print(('player %s wants %s x %s'):format(src, amount, item))
end)

On the server, the global source holds the id of the player who triggered the event. The client does not send it, so it cannot be faked by a client changing its arguments.

Server to client

lua
-- server: to one player
TriggerClientEvent('my_script:client:notify', src, 'Purchase done')

-- server: to everyone
TriggerClientEvent('my_script:client:notify', -1, 'Server restart in 5 minutes')
lua
-- client
RegisterNetEvent('my_script:client:notify', function(message)
    print(message)
end)

The second argument of TriggerClientEvent is always the target. If you forget it, your first data argument is read as the player id and the event goes nowhere.

Save source before you Wait

source is not a value you own, it is a global that the runtime sets for the event that is running. If your handler calls Wait, or calls something that yields such as MySQL.query.await, another event may run in the meantime and source changes.

lua
RegisterNetEvent('my_script:server:buy', function(item, amount)
    local src = source            -- copy it first

    Wait(500)                     -- or any await
    print(src)                    -- still the right player
    print(source)                 -- may be someone else now
end)

Make local src = source the first line of every server handler and use src after that. The same applies inside a callback you pass to another function.

Naming events

Events are global to the whole server: any resource can trigger any event name. If two scripts both register buy or server:buy, both handlers run, and a player can trigger them by name.

Prefix every event with the resource and the side:

lua
'my_script:server:buy'
'my_script:client:updateJob'

Do not reuse the names of other scripts' events, and do not name your event something generic such as giveMoney. A readable name also makes it obvious in the console which script sent what.

Never trust the client

Anything that comes through TriggerServerEvent can be fired by a cheater with any arguments, at any time. Check on the server:

  • that the player is allowed to do it (job, distance, cooldown),
  • that the amounts and item names are valid,
  • that you never take the price or reward from the arguments.
lua
RegisterNetEvent('my_script:server:buy', function(item, amount)
    local src = source
    if type(amount) ~= 'number' or amount < 1 or amount > 10 then return end
    -- look the price up on the server, do not read it from the client
end)

The full list is in securing server events. For a result that must come back to the caller, a callback is cleaner than two events: see server callbacks on ESX, QBCore and ox_lib.

Large or frequent events

Every event is a network message. Do not trigger one every frame, and do not send a very large table to every player. If the server console prints about reliable events overflowing, you are sending too much, too fast: see reliable network event overflow.

Checklist

Symptom Fix
Handler never runs from the other side Use RegisterNetEvent, not only AddEventHandler
TriggerClientEvent reaches nobody Pass a player id, or -1, as the second argument
source is nil or the wrong player local src = source as the first line, before any Wait
Two scripts react to the same event Prefix names with the resource: my_script:server:buy
Client can give itself items or money Validate on the server and look prices up there
Event spam in the console or lag Send less, less often: see reliable network event overflow

Quick answers

What is the difference between RegisterNetEvent and AddEventHandler?

RegisterNetEvent allows the event to be triggered from the other side of the network. AddEventHandler only attaches a function. A handler for a network event needs both, or RegisterNetEvent with the function as its second argument.

How do I send an event to every player?

On the server, TriggerClientEvent('my_script:client:thing', -1, data). The -1 means all connected clients.

Why is source wrong in my server event?

source is a global that holds the player who triggered the current event. If you call Wait and another event runs meanwhile, it changes. Copy it into a local first: local src = source.

Scripts that skip this problem

Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script →Drug Dealer AppStreet sales as an lb-phone app: zones, NPC buyers, levels and police alerts.View script →Shop CreatorBuild a shop in under a minute — owners, employees, vaults and robberies included.View script →

Keep reading