Skip to main content

Garbage Collection

LuaLuaA lightweight, embeddable scripting language used as the primary scripting language in FiveM server development. uses automatic memory management. Two GC modes: incremental (default) and generational (new in 5.4).


GC modes

Incremental (default)

Marks reachable objects, sweeps unreachable ones. Runs in steps interleaved with program execution.

Tuning parameters:

ParameterDefaultMeaning
pause200How much memory can grow before new cycle (%)
stepmul100Speed of GC relative to allocations (%)
stepsize13Bytes between GC steps (~8KB per step)
collectgarbage("setpause", 100) -- collect more aggressively
collectgarbage("setstepmul", 200) -- faster steps

Generational (new in 5.4)

Assumes most objects die young. Collects young objects frequently, old objects rarely.

collectgarbage("generational") -- switch to generational mode
collectgarbage("setminor", 20) -- minor collection threshold (%)
collectgarbage("setmajor", 100) -- major collection threshold (%)

Switch back:

collectgarbage("incremental")

collectgarbage options

CommandDescription
"collect"Full GC cycle
"stop"Stop automatic collection
"restart"Restart automatic collection
"count"Returns memory used (KB), total bytes
"step"Run one GC step. Returns true if cycle finished
"setpause"Set pause parameter
"setstepmul"Set step multiplier
"incremental"Switch to incremental mode
"generational"Switch to generational mode
"isrunning"Returns whether GC is running
-- Check memory usage
local kb, bytes = collectgarbage("count")
print(string.format("%.1f KB", kb))

-- Force full collection
collectgarbage("collect")

-- Measure before/after
collectgarbage("collect")
local before = collectgarbage("count")
-- ... allocate stuff ...
collectgarbage("collect")
local after = collectgarbage("count")
print(string.format("Used: %.1f KB", after - before))

Finalizers (__gc)

Objects with a __gc metamethod get their finalizer called when collected.

local mt = {
__gc = function(self)
print("Finalizing:", self.name)
end
}

do
local obj = setmetatable({name = "resource"}, mt)
-- use obj
end
obj = nil
collectgarbage() -- prints: Finalizing: resource

Rules

  • Finalizer runs once per object
  • Order of finalization is undefined
  • Finalizers can resurrect objects (by storing reference)
  • Set __gc before the object is marked for finalization
-- WRONG: setting __gc after creation
local t = {}
setmetatable(t, {__gc = function() print("done") end})
-- May not work! Set metatable at creation time.

-- CORRECT:
local t = setmetatable({}, {__gc = function() print("done") end})

To-be-closed variables (5.4)

More predictable than __gc:

local function open_file(path)
local f = assert(io.open(path, "r"))
local sentinel = setmetatable({}, {
__close = function()
f:close()
print("File closed: " .. path)
end
})
return f, sentinel
end

do
local f, _ <close> = open_file("test.txt")
-- use f
end -- file closed here, guaranteed

Weak references

Controlled by __mode in the metatable:

ModeMeaning
"k"Weak keys — entry removed when key is collected
"v"Weak values — entry removed when value is collected
"kv"Both weak
-- Weak values (cache pattern)
local cache = setmetatable({}, {__mode = "v"})

function get_data(key)
if not cache[key] then
cache[key] = expensive_compute(key)
end
return cache[key]
end

collectgarbage()
-- Entries with collected values are now gone
-- Weak keys (metadata without preventing GC)
local metadata = setmetatable({}, {__mode = "k"})

function set_meta(obj, data)
metadata[obj] = data
end

-- When obj is collected, its metadata entry is removed

Important notes

  • Only tables can have weak entries
  • Strings, numbers, booleans, nil are never collected
  • Weak tables may have entries disappear at any time after collection

GC phases

1. Marking — mark all reachable objects (from roots)
2. Atomic — handle special cases (weak tables, finalizers)
3. Sweeping — reclaim unreachable objects
4. Finalizing — run __gc on dead objects with finalizers

Performance tips

  1. Don't fight the GC — let it run. Calling collectgarbage("collect") too often wastes CPU.

  2. Avoid creating garbage in hot loops — reuse tables and strings where possible:

    -- BAD: creates new table every frame
    for i = 1, 1000 do
    local t = {x = i, y = i * 2}
    process(t)
    end

    -- BETTER: reuse
    local t = {}
    for i = 1, 1000 do
    t.x = i
    t.y = i * 2
    process(t)
    end
  3. Use weak tables for caches — prevents unbounded memory growth.

  4. Close resources explicitly — don't rely on __gc for deterministic cleanup. Use <close> or explicit close().

  5. Monitor memorycollectgarbage("count") to detect leaks.


Common pitfalls

PitfallSolution
Finalizer runs in arbitrary orderDon't depend on finalization order
Finalizer can resurrect objectSet resurrected reference to nil to allow re-collection
__gc not called on collectcycle you expectMay take 2 cycles; use <close> for deterministic cleanup
Weak table entries disappear unexpectedlyThat's the point — don't assume weak entries persist
Generational mode accumulates old objectsTune major threshold; switch to incremental for full sweep