<!-- fivemad.com, Markdown source of https://fivemad.com/docs/development/client-server-events -->
> Source: https://fivemad.com/docs/development/client-server-events
> Section: Development
> Tags: events, RegisterNetEvent, TriggerServerEvent, TriggerClientEvent, source, callbacks, security, lib.callback
> Last updated: 2026-09-22
> Site map for assistants: https://fivemad.com/llms.txt

# Client, server and events

How FiveM client and server scripts talk: local and net events, TriggerServerEvent, TriggerClientEvent, source, callbacks with ox_lib, and why you never trust the client.

Every FiveM script runs on one of two sides, and the sides can only talk through **events**. Getting this right is the difference between a script that works and a script that gives cheaters free money.

[FiveM Lua Scripting Network Events (CharlesHacks, Zero to Hero Ep. 4)](https://www.youtube.com/watch?v=6-M5o_wiQP0) (YouTube video)

## The two sides

| | Client | Server |
| --- | --- | --- |
| Runs | On each player's PC, inside their game | Once, inside FXServer |
| Knows | Its own player, what's near them, input, the screen | All players, the database, the truth |
| Can do | Draw, read keys, play animations, local effects, most game natives | Kick, ban, save data, send events to anyone, server natives |
| Trust | **None.** Cheaters control it. | Full. |

A resource can have client, server and shared scripts. Shared scripts (config, utility functions) run on both sides, but each side has its own copy: changing `Config.x` on the client does nothing on the server.

## Local events

`TriggerEvent` fires an event on the **same side**, to every resource that listens:

```lua
-- any resource on the same side
AddEventHandler('myres:somethingHappened', function(what)
    print('It happened: ' .. what)
end)

TriggerEvent('myres:somethingHappened', 'a thing')
```

Prefix event names with your resource name (`myres:...`) so they don't clash with other resources.

## Network events

To cross from client to server or back, the receiving side must **register** the event as a net event.

### Client to server

```lua title="client.lua"
RegisterCommand('report', function(_, args)
    local message = table.concat(args, ' ')
    TriggerServerEvent('myres:report', message)
end, false)
```

```lua title="server.lua"
RegisterNetEvent('myres:report', function(message)
    local src = source                 -- who sent it, copy it right away
    if type(message) ~= 'string' or #message == 0 or #message > 200 then return end
    print(('[report] %s (%d): %s'):format(GetPlayerName(src), src, message))
end)
```

`source` is a global that FiveM sets to the **server ID of the player who triggered the event**. Copy it into a local at the top of the handler. After any `Wait` or async call, the global may belong to a different event.

### Server to client

```lua title="server.lua"
TriggerClientEvent('myres:notify', src, 'Your report was sent')   -- one player
TriggerClientEvent('myres:notify', -1, 'Server restart in 5 min') -- everyone
```

```lua title="client.lua"
RegisterNetEvent('myres:notify', function(text)
    BeginTextCommandThefeedPost('STRING')
    AddTextComponentSubstringPlayerName(text)
    EndTextCommandThefeedPostTicker(false, false)
end)
```

`RegisterNetEvent(name, handler)` both allows the event over the network and adds the handler. The older two step form (`RegisterNetEvent(name)` then `AddEventHandler(name, fn)`) does the same thing.

> [!NOTE]
> Without `RegisterNetEvent`, network triggers of that event are ignored. That's a safety feature: only events you explicitly register can be called from the other side.

### Big payloads

Events are for small data. For large data (a big table, a long string), use `TriggerLatentClientEvent(name, target, bytesPerSecond, ...)` or `TriggerLatentServerEvent`, which send it in the background without choking the connection. Or rethink: maybe the client only needs part of it.

## Getting an answer back: callbacks

Events are fire and forget. When the client needs an answer ("can I afford this?"), use a **callback**. FiveM has no built in client/server callback, so use ox_lib's:

```lua title="server.lua"
lib.callback.register('myres:getBalance', function(source)
    local player = exports.qbx_core:GetPlayer(source)   -- or your framework
    return player and player.PlayerData.money.bank or 0
end)
```

```lua title="client.lua"
local balance = lib.callback.await('myres:getBalance', false)
print('Bank balance: ' .. balance)
```

Both files need `shared_script '@ox_lib/init.lua'` in the manifest and `ox_lib` started. QBCore (`QBCore.Functions.CreateCallback` / `TriggerCallback`) and ESX (`ESX.RegisterServerCallback` / `ESX.TriggerServerCallback`) have their own versions.

## Security: never trust the client

A cheater with an executor can call **any** net event you registered, with **any** arguments, as often as they like. So every server handler must assume the worst.

### Bad

```lua title="server.lua (DON'T)"
RegisterNetEvent('fishing:sell', function(amount, price)
    local player = GetPlayer(source)
    player.addMoney(amount * price)     -- client decided the price and the amount
end)
```

### Good

```lua title="server.lua"
local FISH_PRICE = 25
local SELL_POINT = vector3(-1847.0, -1195.0, 14.3)
local lastSell = {}

RegisterNetEvent('fishing:sell', function()
    local src = source
    local player = GetPlayer(src)
    if not player then return end

    -- rate limit
    local now = os.time()
    if lastSell[src] and now - lastSell[src] < 2 then return end
    lastSell[src] = now

    -- position check on the server (OneSync)
    local ped = GetPlayerPed(src)
    if #(GetEntityCoords(ped) - SELL_POINT) > 5.0 then return end

    -- the server counts what the player has
    local count = player.getItemCount('fish')
    if count <= 0 then return end

    player.removeItem('fish', count)
    player.addMoney(count * FISH_PRICE)
end)

AddEventHandler('playerDropped', function()
    lastSell[source] = nil
end)
```

The rules:

1. **The server decides values**: prices, rewards, amounts, item names come from server config, not from arguments.
2. **Use `source`** to know who's asking. Never accept a player ID as an argument to act on "yourself".
3. **Validate arguments**: type, range, length, whitelist (`if not ALLOWED[item] then return end`).
4. **Check the situation**: distance (OneSync lets the server read `GetEntityCoords(GetPlayerPed(src))`), job, cooldown, whether they actually have the item.
5. **Rate limit** anything that gives something.
6. **Don't trigger server events from client "loops"** that give rewards. Give rewards when the server confirms the action.
7. **Log** money and item changes, so you can find abuse later.

## Useful built in events

| Event | Side | When |
| --- | --- | --- |
| `playerConnecting` | Server | A player starts connecting. Lets you defer, check bans, show adaptive cards. |
| `playerJoining` | Server | The player got a server ID and is joining. |
| `playerDropped` | Server | A player left. `source` is the player, first argument is the reason. |
| `onResourceStart` / `onResourceStop` | Both | Any resource started or stopped. Check `GetCurrentResourceName() == resourceName` to react to your own. |
| `onClientResourceStart` | Client | A resource started on this client. |
| `gameEventTriggered` | Client | Game events like `CEventNetworkEntityDamage`. See [Game Damage Events](/tools/damage-events) for kills, deaths and vehicle damage. |

```lua title="server.lua"
AddEventHandler('playerDropped', function(reason)
    local src = source
    print(('%s left: %s'):format(GetPlayerName(src), reason))
end)

AddEventHandler('onResourceStop', function(resourceName)
    if resourceName ~= GetCurrentResourceName() then return end
    -- save state before the resource stops
end)
```

## Commands as the entry point

`RegisterCommand` works on both sides. A client command that needs the server should just send an event, and let the server check permissions:

```lua title="server.lua"
RegisterCommand('heal', function(source, args)
    local target = tonumber(args[1]) or source
    if not IsPlayerAceAllowed(source, 'myres.heal') then return end
    TriggerClientEvent('myres:heal', target)
end, false)
```

```cfg title="server.cfg"
add_ace group.admin myres.heal allow
```

## Common mistakes

- Registering a client event with `AddEventHandler` only, then triggering it from the server: nothing happens, it isn't a net event.
- Using `source` after a `Wait()` or inside a callback: copy it first.
- Sending the full player list or huge tables every second: send changes only, or use [state bags](/docs/development/state-bags-onesync).
- Trusting `GetPlayerName` for identity: names can be anything. Use identifiers or your framework's citizen ID.

Next: [Natives](/docs/development/natives).
