FiveM State Bags — When to Use Them
You’ve got a fuel system script. Every time a player gets in a vehicle, the server fires a TriggerClientEvent to send the fuel level. Player drives across the map, comes back — you fire it again. New player walks up to the same car — you fire it again. Meanwhile you’re tracking who needs what data, managing edge cases when players disconnect mid-drive, and writing boilerplate sync code that you’ll copy-paste into every script you build.
State bags exist specifically to kill this pattern. They’ve been in FiveM since OneSync landed, but I still see developers reaching for events in situations where state bags would cut their code in half. I’ve refactored enough scripts to know when each tool makes sense — and when it doesn’t.
What Are State Bags?
A state bag is a set of key-value pairs attached to an entity — a player, a vehicle, a ped, an object, or the entire server globally. You set a value, and FiveM handles synchronizing it to every client that needs to see it. No manual event triggers, no tracking who’s in range, no worrying about late joiners.
If you’ve used FiveM events before, think of state bags as persistent, auto-syncing data that lives on the entity itself. Events are fire-and-forget messages. State bags are sticky — the data stays until you change it or the entity despawns.
There are three types you need to know.
Entity state attaches to any networked entity. A vehicle’s fuel level, a ped’s faction, an object’s lock status — anything that belongs to a specific thing in the world.
Player state is a special case of entity state for player peds. It’s the most common type you’ll use. Track whether a player is handcuffed, their current job, their duty status.
Global state is server-wide. Think of it as a shared config that every client can read but only the server can write. Time of day overrides, weather locks, server-wide event flags.
The OneSync Requirement
State bags only work with OneSync enabled. If you’re running a server without it in 2026, you have bigger problems — but it’s worth stating explicitly because I’ve seen people debug state bag issues for hours before realizing OneSync was off.
Check your server.cfg:
set onesync on
If that line isn’t there or it’s set to off, nothing in this guide will work. Also, the entities you’re attaching state to must be networked. Local entities that only exist on one client can’t have state bags because there’s nothing to sync.
If you’re setting up a server from scratch, check out the server setup guide first.
Basic Syntax
Let’s start with the fundamentals. Setting and getting state in Lua is straightforward.
Setting Entity State
-- Server-side: set fuel on a vehicle
local vehicle = GetVehiclePedIsIn(GetPlayerPed(source), false)
Entity(vehicle).state.fuel = 80.0
Reading Entity State
-- Client-side: read fuel from a vehicle
local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
local fuel = Entity(vehicle).state.fuel
print("Fuel level: " .. tostring(fuel))
Player State
-- Client-side: set your own state
LocalPlayer.state:set("isHandcuffed", true, true)
-- Server-side: read a player's state
local cuffed = Player(source).state.isHandcuffed
Global State
-- Server-side only: set global state
GlobalState.weatherLocked = true
GlobalState.currentWeather = "RAIN"
-- Client-side: read it
local weather = GlobalState.currentWeather
That third argument in :set() controls replication. By default, server-set state replicates to clients, and client-set state does not replicate. You override this with the boolean:
-- Server: set state but keep it server-only (not sent to clients)
Entity(vehicle).state:set("internalFlag", true, false)
-- Client: set state AND replicate it to the server
LocalPlayer.state:set("taskComplete", true, true)
This is important. If you set state on the client without true as the third argument, the server will never see it.
Listening for Changes
Setting and reading state is half the picture. The real power is reacting to changes with AddStateBagChangeHandler.
-- Client-side: react when any entity's "fuel" state changes
AddStateBagChangeHandler("fuel", nil, function(bagName, key, value, _reserved, replicated)
local entity = GetEntityFromStateBagName(bagName)
if entity == 0 then return end
print("Entity " .. entity .. " fuel changed to " .. tostring(value))
end)
The first argument is the key to watch. The second is a bag name filter — pass nil to watch all entities. The callback fires whenever that key changes on any matching entity.
This is what makes state bags different from events. If a player walks into range of a vehicle that already has fuel = 45.0 set, the handler fires automatically. You don’t need a “player entered scope” event to manually sync the data. It just works.
The Entity Sync Gotcha
There’s a timing issue you need to handle. When AddStateBagChangeHandler fires, the entity might not be fully synced on the client yet. GetEntityFromStateBagName() can return 0 even though the entity exists on the server.
Here’s the pattern I use in every script:
function WaitForEntityFromBag(bagName, timeout)
if not bagName or type(bagName) ~= "string" then return 0 end
if not bagName:find("entity:") then return 0 end
local netId = tonumber(bagName:sub(8))
if not netId then return 0 end
local deadline = GetGameTimer() + (timeout or 2000)
while not NetworkDoesNetworkIdExist(netId) do
Wait(100)
if GetGameTimer() > deadline then return 0 end
end
return NetworkGetEntityFromNetworkId(netId)
end
-- Usage in a handler
AddStateBagChangeHandler("faction", nil, function(bagName, key, value)
local entity = WaitForEntityFromBag(bagName)
if entity == 0 then return end
-- Now safe to use the entity
end)
Without this, you’ll get intermittent bugs where handlers seem to “miss” entities. It’s the single most common state bag mistake I see in open-source scripts.
When to Use State Bags vs Events
This is where most developers get confused. Both tools sync data between server and client, so when do you pick one over the other?
Use state bags when:
The data describes a persistent property of an entity. Fuel level, job status, handcuff state, vehicle ownership, door lock status — anything where a late-joining player or a player entering scope needs to immediately know the current value. If you’d have to write “on player join, send them the current X” logic, state bags eliminate that entirely.
Use events when:
The action is a one-time command, not persistent data. “Play this animation,” “show this notification,” “open this menu.” Events are for things that happen once and don’t need to be replayed for late joiners. If a player walks into range after the animation played, they shouldn’t see it — that’s an event, not state.
Use latent events when:
You need to transfer large payloads. State bags have size limits and are rate-limited. If you’re sending a full inventory table with 50 items, serialized JSON, or big config dumps, use latent events instead. State bags are designed for small, frequently-updated values.
Here’s a concrete example. Say you’re building a drug system — something like our LMX Trap & Stores script. The current stock of a trap house? That’s state bag material. It’s a persistent value that any player walking up needs to see. But the notification that says “You sold 5 bags of coke” — that’s an event. It fires once, for one player, and nobody else needs to know about it later.
The Shallow Copy Trap
The official docs mention this but don’t emphasize it enough. State bag getters and setters are naive. Every get deserializes the entire value, and every set serializes the entire value back.
This means nested tables are a trap:
-- BAD: This does NOT replicate
Entity(vehicle).state.stats = { fuel = 80, mileage = 1200 }
Entity(vehicle).state.stats.fuel = 75 -- This modifies a local copy, not the state bag
-- GOOD: Use flat keys
Entity(vehicle).state["stats:fuel"] = 80
Entity(vehicle).state["stats:mileage"] = 1200
-- Also GOOD: Re-set the entire table
local stats = Entity(vehicle).state.stats or {}
stats.fuel = 75
Entity(vehicle).state.stats = stats -- This replicates because it's a direct set
If you’re reading the same state multiple times, cache it locally:
-- BAD: Deserializes twice
local fuel = Entity(vehicle).state.stats.fuel
local miles = Entity(vehicle).state.stats.mileage
-- GOOD: Deserialize once
local stats = Entity(vehicle).state.stats
local fuel = stats.fuel
local miles = stats.mileage
This matters for performance. If you’re reading state every frame in a render loop, those extra deserializations add up. Check my performance guide if you want to understand how to measure the impact with resmon.
Rate Limiting
State bags are rate-limited by FiveM. If you spam updates too fast, the server will throttle or drop them. This catches people off guard when they try to sync something every frame.
-- TERRIBLE: Don't do this
CreateThread(function()
while true do
Wait(0) -- Every frame
Entity(vehicle).state.speed = GetEntitySpeed(vehicle)
end
end)
-- ACCEPTABLE: Throttle your updates
CreateThread(function()
while true do
Wait(500) -- Twice per second is usually enough
local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)
if vehicle ~= 0 then
Entity(vehicle).state.speed = math.floor(GetEntitySpeed(vehicle) * 100) / 100
end
end
end)
For values that change continuously like speed or position, ask yourself whether you actually need to sync them. GTA already syncs entity positions natively. You probably don’t need a state bag for something the engine handles.
Security and Write Policies
State bags follow a simple permission model. The server can write to any state bag. Clients can only write to state bags belonging to entities they own — their player ped and entities they’re the network owner of.
Clients cannot write to GlobalState. Period. If you need client input to affect global state, the client sends a server event, the server validates it, and the server sets the global state. Same pattern you’d use with any server-authoritative system.
-- Client: request a change
TriggerServerEvent("weather:request", "CLEAR")
-- Server: validate and apply
RegisterNetEvent("weather:request", function(weather)
local src = source
if not IsPlayerAdmin(src) then return end
GlobalState.currentWeather = weather
end)
This is critical for preventing exploits. Never trust client-set state for anything gameplay-critical without server validation. A modder can set LocalPlayer.state:set("isAdmin", true, true) — if your server reads that without checking, you’ve got a problem.
Real-World Patterns
Let me walk through a few patterns I use constantly.
Vehicle Lock Status
-- Server-side: when a player locks their car
RegisterNetEvent("vehicle:toggleLock", function(netId)
local src = source
local vehicle = NetworkGetEntityFromNetworkId(netId)
if vehicle == 0 then return end
local currentState = Entity(vehicle).state.locked or false
Entity(vehicle).state.locked = not currentState
end)
-- Client-side: check if a vehicle is locked before entry
local locked = Entity(vehicle).state.locked
if locked then
-- Show "vehicle is locked" notification
end
Job Duty Status
-- Server: toggle duty
RegisterNetEvent("job:toggleDuty", function()
local src = source
local onDuty = Player(src).state.onDuty or false
Player(src).state.onDuty = not onDuty
end)
-- Client: other players can see if someone is on duty
AddStateBagChangeHandler("onDuty", nil, function(bagName, key, value)
if bagName:find("player:") then
-- Update name tag, uniform, blip, etc.
end
end)
Server-Wide Event Flags
-- Server: start a server event
GlobalState.purgeActive = true
-- Client: react to it
AddStateBagChangeHandler("purgeActive", "global", function(_, _, value)
if value then
-- Enable PVP, change sky color, play siren
else
-- Restore normal state
end
end)
These patterns cover probably 80% of what you’ll use state bags for. The rest comes down to your specific scripts.
Common Mistakes
I see these constantly when auditing scripts or helping people in our Discord.
Forgetting the replication flag. Client-side state doesn’t replicate by default. If the server can’t see your state, you probably forgot :set("key", value, true).
Using state bags for large data. Don’t store entire inventory tables or vehicle modification lists in a single state bag key. Break them into smaller keys or use a different sync method entirely. The serialization overhead will hurt you.
Not handling the entity sync delay. Always use a wait-for-entity pattern in your AddStateBagChangeHandler callbacks. Entities aren’t always ready when the handler fires.
Updating every frame. State bags are not designed for real-time continuous data. Throttle your updates. If you need frame-perfect sync, you’re probably looking for native entity sync, not state bags.
Treating state bags like a database. State bags exist only as long as the entity exists. When a vehicle despawns, its state bags are gone. For persistent data, use your database. State bags are runtime sync, not storage.
When State Bags Don’t Make Sense
State bags aren’t always the answer. Here’s when you should reach for something else.
If you need to sync data about entities that don’t exist yet — like pre-loading config data for a shop that hasn’t spawned — use events or shared config files.
If you need complex queries or filtering across many entities — “find all vehicles with fuel below 20%” — state bags don’t support queries. You’d need to loop through all entities and check each one, which defeats the purpose. Track that data server-side in a table.
If the data is truly massive, use latent events. State bags serialize everything through the game’s networking layer, which has bandwidth constraints.
For beginner scripters still getting comfortable with client-server architecture, I’d recommend starting with the Lua scripting tutorial and the events guide before diving into state bags. They build on those concepts.
Also worth checking out our free scripts collection — several of them use state bags in practice, so you can see real implementations instead of just reading about the API.
Wrapping Up
State bags solve a specific problem really well: syncing small, persistent, entity-bound data without writing manual event plumbing. They handle late joiners, scope changes, and replication automatically. But they’re not a replacement for events, databases, or shared config — they’re another tool in the box.
Use them for entity properties. Use events for one-time actions. Use your database for persistence. Get the boundaries right and your scripts will be cleaner, faster, and way easier to debug.