Resources in FiveM follow a specific lifecycle managed by the server.
Every resource needs an fxmanifest.lua file:
fx_version 'cerulean'
game 'gta5'
author 'Your Name'
description 'Resource Description'
version '1.0.0'
-- Server scripts
server_scripts {
'server.lua'
}
-- Client scripts
client_scripts {
'client.lua'
}
-- Shared scripts
shared_scripts {
'config.lua'
}Resource is not loaded. Default state.
Resource is being loaded:
Resource is running:
Resource is shutting down:
Ensures resource is started:
ensure myresourceStart resource (if stopped):
start myresourceStop resource:
stop myresourceRestart resource:
restart myresourceLifecycle events allow your resource to respond to start and stop events. Always check if the event is for your resource to avoid executing code for other resources.
Fired when a resource starts (including your own). Use this for initialization logic:
-- server.lua
-- Initialize database connection
local dbReady = false
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
print('^2[MyResource]^7 Starting initialization...')
-- Load configuration
Config = require 'config'
-- Initialize database connection
MySQL.ready(function()
dbReady = true
print('^2[MyResource]^7 Database connected')
end)
-- Load data from database
MySQL.query('SELECT * FROM my_table', {}, function(result)
if result then
print('^2[MyResource]^7 Loaded ' .. #result .. ' records')
end
end)
-- Register server exports
exports('myFunction', function()
return 'Hello from server'
end)
print('^2[MyResource]^7 Resource started successfully')
end
end)Common initialization tasks:
Fired when a resource stops (including your own). Use this for cleanup:
-- server.lua
-- Store active connections
local activeConnections = {}
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
-- Initialize connections
activeConnections = {}
end
end)
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
print('^3[MyResource]^7 Stopping resource, cleaning up...')
-- Save data to database
for playerId, data in pairs(activeConnections) do
MySQL.update('UPDATE players SET data = ? WHERE id = ?', {
json.encode(data),
playerId
})
end
-- Close database connections (if using connection pool)
-- MySQL.close() -- Only if you manage connections manually
-- Cancel all timers
-- (FiveM automatically cleans up timers, but you can track them)
-- Clear exports
-- (FiveM automatically removes exports)
print('^3[MyResource]^7 Cleanup complete')
end
end)Common cleanup tasks:
Fired when a resource starts on the client. Use this for client initialization:
-- client.lua
local isResourceReady = false
AddEventHandler('onClientResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
print('^2[MyResource]^7 Client starting...')
-- Initialize NUI
SetNuiFocus(false, false)
-- Load configuration
Config = require 'config'
-- Request server data
TriggerServerEvent('myresource:requestData')
-- Register key mappings
RegisterKeyMapping('myresource:open', 'Open My Resource', 'keyboard', 'F5')
-- Initialize blips or markers
CreateThread(function()
while true do
Wait(0)
-- Draw markers, blips, etc.
end
end)
isResourceReady = true
print('^2[MyResource]^7 Client ready')
end
end)Fired when a resource stops on the client. Use this for client cleanup:
-- client.lua
AddEventHandler('onClientResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
print('^3[MyResource]^7 Client stopping...')
-- Close NUI if open
SetNuiFocus(false, false)
SendNUIMessage({ type = 'close' })
-- Remove blips
if DoesBlipExist(myBlip) then
RemoveBlip(myBlip)
end
-- Clean up markers/threads
-- (FiveM automatically stops threads, but clean up state)
-- Save local state (if needed)
-- TriggerServerEvent('myresource:saveState', localState)
print('^3[MyResource]^7 Client cleanup complete')
end
end)Here’s a complete example showing proper lifecycle management:
-- server.lua
local resourceName = GetCurrentResourceName()
local playerData = {}
local cleanupTasks = {}
-- Initialize on start
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
print(string.format('^2[%s]^7 Initializing...', resourceName))
-- Load configuration
Config = require 'config'
-- Initialize database
MySQL.ready(function()
print(string.format('^2[%s]^7 Database ready', resourceName))
end)
-- Start periodic task
local taskId = CreateThread(function()
while true do
Wait(60000) -- Every minute
-- Periodic task
end
end)
table.insert(cleanupTasks, taskId)
-- Register exports
exports('getPlayerData', function(playerId)
return playerData[playerId]
end)
print(string.format('^2[%s]^7 Started successfully', resourceName))
end
end)
-- Cleanup on stop
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
print(string.format('^3[%s]^7 Stopping...', resourceName))
-- Save all player data
for playerId, data in pairs(playerData) do
MySQL.update('UPDATE players SET data = ? WHERE id = ?', {
json.encode(data),
playerId
})
end
-- Cleanup tasks are handled automatically by FiveM
-- But you can track and cancel them if needed
print(string.format('^3[%s]^7 Stopped', resourceName))
end
end)When you use the restart command, FiveM reloads the resource without fully stopping the server. Understanding what persists and what resets is crucial.
These remain unchanged when you restart a resource:
These are reset when you restart a resource:
CreateThread and Wait threads are stopped-- ❌ BAD: Data lost on restart
local playerMoney = {} -- Lost on restart!
-- ✅ GOOD: Load from database on start
local playerMoney = {}
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
-- Load from database
MySQL.query('SELECT id, money FROM players', {}, function(result)
for _, player in ipairs(result) do
playerMoney[player.id] = player.money
end
end)
end
end)-- Save state function
local function saveAllState()
for playerId, data in pairs(playerData) do
MySQL.update('UPDATE players SET data = ? WHERE id = ?', {
json.encode(data),
playerId
})
end
end
-- Save on stop
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
saveAllState()
end
end)
-- Also save periodically
CreateThread(function()
while true do
Wait(300000) -- Every 5 minutes
saveAllState()
end
end)-- Notify clients before restart (if possible)
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
-- Notify all players
TriggerClientEvent('myresource:restarting', -1)
-- Small delay to let clients process
Wait(1000)
-- Then do cleanup
-- ...
end
end)-- If using QBCore/ESX, use framework events for player data
-- These persist across resource restarts
-- ❌ BAD: Storing player data in resource
local playerData = {} -- Lost on restart
-- ✅ GOOD: Use framework exports
local QBCore = exports['qb-core']:GetCoreObject()
-- Framework handles persistenceAlways test that your resource handles restarts correctly:
-- Add a test command
RegisterCommand('testrestart', function(source, args)
if source == 0 then -- Console only
print('Testing restart behavior...')
-- Save current state
-- Restart resource
ExecuteCommand('restart ' .. GetCurrentResourceName())
end
end, true) -- Restricted commandAssuming state persists:
-- ❌ WRONG
local counter = 0
-- Counter resets to 0 on every restart!
-- ✅ CORRECT
-- Store in database or use frameworkNot cleaning up on stop:
-- ❌ WRONG: Data lost if server crashes
-- (No cleanup handler)
-- ✅ CORRECT: Save on stop
AddEventHandler('onResourceStop', function(resourceName)
if resourceName == GetCurrentResourceName() then
-- Save everything
end
end)Race conditions on start:
-- ❌ WRONG: Using data before it's loaded
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
MySQL.query('SELECT * FROM table', {}, function(result)
-- This is async, might not be ready immediately
end)
-- Using result here will fail!
end
end)
-- ✅ CORRECT: Wait for data to load
AddEventHandler('onResourceStart', function(resourceName)
if resourceName == GetCurrentResourceName() then
MySQL.ready(function()
-- Now database is ready
MySQL.query('SELECT * FROM table', {}, function(result)
-- Use result here
end)
end)
end
end)After implementing lifecycle events:
Test resource start:
Test resource stop:
stop myresource commandTest restart:
restart myresource commandCheck for memory leaks: