Skip to main content
TRACK B·BUILD REAL PRODUCTS·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this lesson plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.

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.

You'll ship
Cleanup handlers for resource stop, player drop, and timed auto-delete.
Time
~25 minutes
You'll learn
Signs of entity leaks, cleanup on resource stop, cleanup on player drop, timeout auto-delete, and why the drop path has to live on the server.
Prereqs
Spawning entities (the registry pattern) and OneSync enabled.
BEFORE YOU START

Build it

Make the resource folder

The server has one folder for this lesson.

Inside your server's resources folder, create this folder:

code
resources/qu_cleanup_discipline

Create the files

Every file named in the manifest exists.

Create this exact file layout. Note the new server.lua compared to spawning entities:

code
resources/qu_cleanup_discipline/
fxmanifest.lua
client.lua
server.lua

Write fxmanifest.lua

FiveM knows which files to load.

Open fxmanifest.lua and paste this:

code
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

The client spawns a networked box and reports it to the server.

Open client.lua and paste this:

code
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

The server tracks each player's boxes and deletes them on drop.

Open server.lua and paste this:

code
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

The expected proof appears in the correct console.

Open server.cfg and add this line:

code
ensure qu_cleanup_discipline

Save, then run:

code
restart qu_cleanup_discipline

Run this test fast, before the 30 second timeout fires:

code
/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.

Still ahead in this lesson
  • 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.