Cleanup discipline
Spawning is easy. Cleaning up is the part that separates scripts that work from scripts that destroy servers. If you spawn entities and never delete them, the world fills with ghost objects, entity counts climb, and the server lags or crashes. This lesson teaches the three moments cleanup has to happen, and why one of them cannot be done from the client at all.
Build it
Make the resource folder
Inside your server's resources folder, create this folder:
resources/qu_cleanup_discipline
Create the files
Create this exact file layout. Note the new server.lua compared to spawning entities:
resources/qu_cleanup_discipline/
fxmanifest.lua
client.lua
server.lua
Write fxmanifest.lua
Open fxmanifest.lua and paste this:
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'
dependencies {
'/onesync'
}
The dependencies { '/onesync' } line tells the server to refuse to start this resource unless OneSync (state awareness) is on. The drop-cleanup path later in this lesson resolves networked entities on the server, which only works with state awareness enabled, so this guard fails loudly at start instead of silently misbehaving later.
There is no lua54 'yes' line. As of June 2025 that directive is deprecated and ignored: Lua 5.4 is the only Lua runtime now, so you leave it out.
Write the client code
Open client.lua and paste this:
local objects = {}
RegisterCommand('spawnbox', function()
local model = joaat('prop_boxpile_07d')
if not IsModelInCdimage(model) or not IsModelValid(model) then return end
RequestModel(model)
local deadline = GetGameTimer() + 5000
while not HasModelLoaded(model) do
if GetGameTimer() > deadline then
print('[qu_cleanup_discipline] model load timed out')
return
end
Wait(0)
end
local coords = GetEntityCoords(PlayerPedId())
local object = CreateObject(model, coords.x + 2.0, coords.y, coords.z, true, true, false)
objects[object] = true
SetModelAsNoLongerNeeded(model)
local netId = NetworkGetNetworkIdFromEntity(object)
TriggerServerEvent('qu_cleanup:registerBox', netId)
print('[qu_cleanup_discipline] spawned object ' .. object .. ' netId ' .. netId)
SetTimeout(30000, function()
if DoesEntityExist(object) then
DeleteEntity(object)
objects[object] = nil
print('[qu_cleanup_discipline] timeout deleted object ' .. object)
end
end)
end, false)
RegisterCommand('clearboxes', function()
for object in pairs(objects) do
if DoesEntityExist(object) then DeleteEntity(object) end
objects[object] = nil
end
TriggerServerEvent('qu_cleanup:clearMine')
print('[qu_cleanup_discipline] registry cleared')
end, false)
AddEventHandler('onResourceStop', function(name)
if name == GetCurrentResourceName() then
for object in pairs(objects) do
if DoesEntityExist(object) then DeleteEntity(object) end
end
end
end)
Write the server code
Open server.lua and paste this:
local BOX_MODEL = joaat('prop_boxpile_07d')
local MAX_BOXES_PER_PLAYER = 10
local boxesByPlayer = {}
local function countBoxes(owned)
local count = 0
for _ in pairs(owned or {}) do count = count + 1 end
return count
end
local function deleteTracked(src, reason)
local owned = boxesByPlayer[src]
if not owned then return 0 end
local deleted = 0
for netId in pairs(owned) do
local entity = NetworkGetEntityFromNetworkId(netId)
if entity ~= 0 and DoesEntityExist(entity) then
DeleteEntity(entity)
deleted = deleted + 1
end
end
boxesByPlayer[src] = nil
print(('[qu_cleanup_discipline] %s player %s, deleted %s boxes'):format(reason, src, deleted))
return deleted
end
RegisterNetEvent('qu_cleanup:registerBox', function(netId)
local src = source
netId = tonumber(netId)
if not netId then return end
local entity = NetworkGetEntityFromNetworkId(netId)
if entity == 0 or not DoesEntityExist(entity) then return end
if NetworkGetEntityOwner(entity) ~= src then return end
if GetEntityModel(entity) ~= BOX_MODEL then return end
boxesByPlayer[src] = boxesByPlayer[src] or {}
if countBoxes(boxesByPlayer[src]) >= MAX_BOXES_PER_PLAYER then
DeleteEntity(entity)
return
end
boxesByPlayer[src][netId] = true
print('[qu_cleanup_discipline] tracking netId ' .. netId .. ' for player ' .. src)
SetTimeout(30000, function()
local owned = boxesByPlayer[src]
if not owned or not owned[netId] then return end
local trackedEntity = NetworkGetEntityFromNetworkId(netId)
if trackedEntity ~= 0 and DoesEntityExist(trackedEntity) then
DeleteEntity(trackedEntity)
end
owned[netId] = nil
if next(owned) == nil then boxesByPlayer[src] = nil end
end)
end)
RegisterNetEvent('qu_cleanup:clearMine', function()
deleteTracked(source, 'cleared')
end)
AddEventHandler('playerDropped', function()
local src = source
deleteTracked(src, 'dropped')
end)
Start and test it
Open server.cfg and add this line:
ensure qu_cleanup_discipline
Save, then run:
restart qu_cleanup_discipline
Run this test fast, before the 30 second timeout fires:
/spawnbox then /clearboxes
The spawn print and the tracking print share one handle and net id pair, so your real numbers will differ from the sample below. The point is the shape: a spawn line, a server tracking line, and a cleared line, with no timeout line because you cleared first. The server may report deleted 0 boxes even though cleanup worked: the client deletes its box locally first and then fires the event, so the server can find nothing left to delete by the time it resolves the net id. A zero count here means the client already cleaned up, not that cleanup failed.
Keep reading the full lesson
Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.
- How it works
- If something went wrong
- What you can do now
- Try it yourself
The remainder of Cleanup discipline is available to FiveM School members.