FiveM Events — Client, Server & Net Events
You’re writing FiveM scripts and things work… sometimes. You fire an event on the client and nothing happens on the server. You trigger a net event and suddenly every player on the server gets the callback. Or worse, you leave a net event wide open and some kid with a mod menu starts giving himself money.
Events are the backbone of how FiveM scripts communicate. Every framework — ESX, QBCore, Qbox — is built on top of them. If you don’t understand events properly, you’re either going to write broken scripts or insecure ones. I’ve seen both destroy servers.
This guide breaks down exactly how events work in FiveM, when to use each type, and how to avoid the mistakes I see in almost every open-source resource I audit.
What Even Is an Event?
An event in FiveM is just a named signal. One piece of code says “hey, this thing happened” and any other piece of code that’s listening for that name responds to it.
Think of it like a radio frequency. Someone broadcasts on channel “player:spawned” and anyone tuned into that channel hears it and can react.
There are two fundamental operations:
-- Broadcasting (triggering)
TriggerEvent("something:happened", someData)
-- Listening (handling)
AddEventHandler("something:happened", function(data)
print("Got it: " .. data)
end)
Simple enough. But FiveM has multiple environments — client and server — and that’s where things get interesting.
The Three Environments
Before diving into event types, you need to understand where your code runs. If this is fuzzy for you, go read my Lua scripting tutorial first — it covers the basics of client vs server scripts.
Client-side runs on each player’s machine. It handles things they can see: drawing markers, playing animations, rendering UI. Each player has their own client instance.
Server-side runs on your actual server box. It handles the truth: database writes, money transactions, inventory changes, validation. There’s one server instance shared by everyone.
The network is the bridge between them. Net events are how a client talks to the server and how the server talks back to clients.
Local Events: Same-Side Communication
Local events stay within the same environment. Client-to-client or server-to-server. They never cross the network.
Client-Side Local Events
-- script_a/client.lua
TriggerEvent("hud:showNotification", "You picked up an item")
-- script_b/client.lua
AddEventHandler("hud:showNotification", function(msg)
-- draw notification on screen
SetNotificationTextEntry("STRING")
AddTextComponentString(msg)
DrawNotification(false, false)
end)
Both of these run on the same player’s client. Script A fires the event, Script B handles it. The server never knows this happened. Other players never know this happened.
When to use this: Cross-resource communication on the same side. Your HUD script listening for notifications from your job script. Your phone UI reacting to incoming call data. Anything where two scripts on the same machine need to talk.
Server-Side Local Events
-- script_a/server.lua
TriggerEvent("log:action", source, "purchased_vehicle", vehicleModel)
-- script_b/server.lua
AddEventHandler("log:action", function(playerId, action, detail)
-- write to Discord webhook or database log
end)
Same concept, but on the server. Both scripts run server-side. No network traffic involved.
When to use this: Logging systems, shared utilities, anything where multiple server scripts need to coordinate without involving clients.
Net Events: Crossing the Network
This is where most people get confused, and where most security holes come from.
Net events send data between the client and server over the network. There are two directions and they use different functions.
Client to Server
The client wants to tell the server something happened. Maybe the player finished a minigame, wants to buy an item, or opened a shop.
-- client.lua
TriggerServerEvent("shop:buyItem", "water", 3)
-- server.lua
RegisterNetEvent("shop:buyItem")
AddEventHandler("shop:buyItem", function(itemName, quantity)
local src = source -- the player who triggered this
-- validate and process the purchase
end)
TriggerServerEvent sends data from the client to the server. On the server side, you must call RegisterNetEvent before adding the handler. This tells FiveM “yes, this event is allowed to come from the network.” Without RegisterNetEvent, the handler will only respond to local server events.
The source variable is automatically set by FiveM to the server ID of the player who triggered the event. You don’t pass it — it’s injected. This is important for security, which I’ll get to in a minute.
Server to Client
The server wants to tell a specific client (or all clients) something happened.
-- server.lua: send to one player
TriggerClientEvent("ui:showReward", targetPlayerId, "You earned $500")
-- server.lua: send to ALL players
TriggerClientEvent("ui:announcement", -1, "Server restart in 5 minutes")
-- client.lua
RegisterNetEvent("ui:showReward")
AddEventHandler("ui:showReward", function(message)
-- show reward popup
end)
RegisterNetEvent("ui:announcement")
AddEventHandler("ui:announcement", function(message)
-- show server-wide announcement
end)
TriggerClientEvent takes a player ID as the first argument. Pass -1 to broadcast to every connected player. On the client side, you again need RegisterNetEvent to allow receiving the event from the network.
The Modern Shorthand
In modern FiveM scripting, you can combine RegisterNetEvent and AddEventHandler into one call:
-- This is cleaner and what most scripts use now
RegisterNetEvent("shop:buyItem", function(itemName, quantity)
local src = source
-- handle it
end)
Same result, less boilerplate. I use this form in almost everything I write now.
The Security Problem Nobody Talks About Enough
Here’s the part that matters most and that I see developers mess up constantly.
Any registered net event can be triggered by any client. Read that again. If you have a server-side net event called admin:giveItem, any player with a mod menu or executor can fire TriggerServerEvent("admin:giveItem", "weapon_rpg", 99) and if your handler doesn’t validate properly, they just gave themselves 99 RPGs.
Rule 1: Never Trust the Client
The client is compromised territory. Assume every TriggerServerEvent call could be coming from a cheater. Always validate on the server.
-- BAD: trusting whatever the client sends
RegisterNetEvent("bank:withdraw", function(amount)
local src = source
-- just gives them the money, no questions asked
exports.oxmysql:execute("UPDATE users SET bank = bank - ? WHERE id = ?", {amount, src})
TriggerClientEvent("bank:success", src, amount)
end)
-- GOOD: validating everything server-side
RegisterNetEvent("bank:withdraw", function(amount)
local src = source
-- Is this even a number?
if type(amount) ~= "number" then return end
-- Is it positive and reasonable?
if amount <= 0 or amount > 1000000 then return end
-- Does the player actually have this money?
local player = GetPlayerData(src)
if not player or player.bank < amount then return end
-- Now it's safe to process
player.bank = player.bank - amount
player.cash = player.cash + amount
TriggerClientEvent("bank:success", src, amount)
end)
Rule 2: Never Send Sensitive Data to the Client
Don’t trigger client events with data that players shouldn’t have access to. Mod menus can intercept incoming events and read the payload.
-- BAD: sending everyone's cash balance to a client
TriggerClientEvent("scoreboard:update", playerId, allPlayerData)
-- GOOD: only send what they need to see
TriggerClientEvent("scoreboard:update", playerId, sanitizedPublicData)
Rule 3: Rate Limit Critical Events
A cheater can spam TriggerServerEvent hundreds of times per second. If your handler does a database query each time, they’ll crash your MySQL.
local cooldowns = {}
RegisterNetEvent("atm:withdraw", function(amount)
local src = source
-- rate limit: one withdrawal per 2 seconds
if cooldowns[src] and (os.time() - cooldowns[src]) < 2 then
return
end
cooldowns[src] = os.time()
-- process the withdrawal...
end)
If you’re running into performance issues from event spam or heavy scripts, my resmon performance guide covers how to diagnose what’s eating your frame budget.
How Frameworks Use Events
Every major framework is essentially a layer of events on top of FiveM’s core. Understanding this makes everything click.
ESX
ESX is heavily event-driven. If you’ve ever written TriggerServerEvent("esx:getSharedObject") in an older ESX script, that’s a net event fetching the framework’s core object. Modern ESX uses exports instead, but the internal communication between ESX modules still runs on events.
-- Classic ESX pattern (older versions)
TriggerEvent("esx:getSharedObject", function(obj) ESX = obj end)
-- Modern ESX (1.9+)
ESX = exports.es_extended:getSharedObject()
QBCore
QBCore follows a similar pattern. The core object is fetched via an export, but internal systems like notifications, phone calls, and inventory actions use net events extensively.
-- QBCore notification event
TriggerClientEvent("QBCore:Notify", src, "You don't have enough money", "error")
-- QBCore server callback (event-based RPC)
QBCore.Functions.CreateCallback("police:getPlayers", function(source, cb)
local players = QBCore.Functions.GetPlayers()
cb(players)
end)
Qbox
Qbox builds on top of QBCore’s event architecture but introduces better bridging. If you’re deciding between frameworks, check out my ESX vs QBCore vs Qbox comparison — the event system differences are one of the reasons people migrate.
The key takeaway: when you’re debugging framework issues, check the events. If a job isn’t working, an item isn’t registering, or a notification isn’t showing, nine times out of ten it’s an event that’s either not being triggered or not being listened to.
Callbacks: Events With Return Values
Plain events are fire-and-forget. You trigger them and move on. But often you need a response — “does this player have enough money?” or “what’s in this player’s inventory?”
Frameworks solve this with callbacks, which are just a pair of events wrapped in a nice API. The client sends a request event, the server processes it, and sends a response event back.
-- Under the hood, a callback is roughly this:
-- Client sends: TriggerServerEvent("callback:request", callbackId, ...)
-- Server responds: TriggerClientEvent("callback:response", src, callbackId, result)
You don’t write this manually. QBCore has QBCore.Functions.CreateCallback and QBCore.Functions.TriggerCallback. ESX has ESX.RegisterServerCallback and ESX.TriggerServerCallback. ox_lib has lib.callback which works across frameworks.
-- ox_lib callback (framework-agnostic, recommended)
-- server
lib.callback.register("getPlayerVehicles", function(source)
local vehicles = MySQL.query.await("SELECT * FROM player_vehicles WHERE citizenid = ?", {citizenid})
return vehicles
end)
-- client
local vehicles = lib.callback.await("getPlayerVehicles")
-- vehicles is now available, no nested callbacks
If you’re doing database-heavy work with callbacks, make sure your queries are optimized. My database design guide covers indexing and query patterns that prevent your callbacks from becoming bottlenecks.
Common Mistakes I See Every Week
Triggering Events in Loops
-- DON'T do this
Citizen.CreateThread(function()
while true do
Wait(0)
TriggerServerEvent("player:updatePosition", GetEntityCoords(PlayerPedId()))
end
end)
This sends a net event to the server 60 times per second. Multiply that by 64 players and your server is processing 3,840 position updates per second. Use state bags or increase the wait interval significantly.
Forgetting RegisterNetEvent
-- This handler will never fire from the network
AddEventHandler("shop:buyItem", function(item)
-- nothing happens when a client triggers this
end)
-- You need RegisterNetEvent first
RegisterNetEvent("shop:buyItem")
AddEventHandler("shop:buyItem", function(item)
-- now it works
end)
This is probably the single most common “my script doesn’t work” problem. If your event handler isn’t firing, check this first.
Using the Same Event Name in Multiple Resources
-- resource_a/server.lua
RegisterNetEvent("giveItem", function(item)
-- resource A's handler
end)
-- resource_b/server.lua
RegisterNetEvent("giveItem", function(item)
-- resource B's handler — BOTH fire when triggered
end)
Both handlers will execute. This is by design — FiveM doesn’t limit events to one handler. But it causes chaos when you don’t expect it. Always namespace your events with your resource name.
-- Much better
RegisterNetEvent("ybn-shop:giveItem", function(item) end)
RegisterNetEvent("qb-inventory:giveItem", function(item) end)
Passing Entities Across the Network
-- BAD: entity handles are local to each client
TriggerServerEvent("vehicle:store", vehicle)
-- GOOD: use net IDs for entities
local netId = NetworkGetNetworkIdFromEntity(vehicle)
TriggerServerEvent("vehicle:store", netId)
-- On the server, convert back
RegisterNetEvent("vehicle:store", function(netId)
local vehicle = NetworkGetEntityFromNetworkId(netId)
-- now you have the server-side entity handle
end)
Entity handles are different on every client and on the server. If you pass a raw entity handle across the network, it means nothing to the recipient. Always convert to network IDs first.
Performance Tips for Events
Events aren’t free. Every net event creates network traffic, and every handler burns CPU when it fires. Here are the rules I follow:
Batch updates instead of spamming events. If you need to update multiple things, send one event with a table of data instead of five separate events.
Use state bags for synced data. If you’re constantly sending position, health, or status updates between client and server, state bags are built for this. They handle replication automatically and more efficiently than manual net events. I’ll cover state bags in depth in a future post.
Don’t register events you don’t need. Every RegisterNetEvent is an entry point. If a client event is only needed during a specific activity, consider using RemoveEventHandler when it’s no longer needed.
Keep payloads small. Don’t send entire database rows through events when you only need two fields. Serialize only what’s necessary.
Quick Reference Table
Here’s a cheat sheet for which function to use and when:
Same-side communication (no network):
TriggerEvent(name, ...) fires the event locally. AddEventHandler(name, fn) catches it. Works identically on client and server for local events.
Client to Server:
Client calls TriggerServerEvent(name, ...). Server uses RegisterNetEvent(name, fn) to handle it. The source variable gives you the sender’s player ID.
Server to Client:
Server calls TriggerClientEvent(name, playerId, ...). Use -1 as the player ID to broadcast to everyone. Client uses RegisterNetEvent(name, fn) to handle it.
Server to All Clients (near a point):
Server calls TriggerClientEvent with -1 or use TriggerLatentClientEvent for large payloads that should be sent over multiple frames to avoid network spikes.
Where to Go From Here
Events are foundational. Once you understand them, everything else in FiveM development makes more sense — scripts you install become readable, framework code stops being magic, and debugging goes from guesswork to methodical.
If you’re building custom jobs or interactive systems, events are how players interact with your scripts. Take a look at our LMX Trap & Stores script for an example of how a well-structured event system handles complex player interactions across multiple locations. For restaurant and food service systems, LMX RestaurantMaster uses event-driven workflows for cooking, serving, and billing.
Want to see scripts that use these patterns well? Check out our free scripts collection — they’re all open source and a good way to study real event usage in production resources.
If you get stuck or want to show off what you’ve built, join the YBN Scripts Discord. There’s always someone around who’s dealt with whatever event-related headache you’re running into.