Skip to content
Garbage Collector Tuning
step 1/4

Reading — step 1 of 4

Learn

~2 min readGC, JIT, and the C Bridge

Lua uses a generational (5.4+) or incremental mark-and-sweep (5.1-5.3) garbage collector. You rarely think about it — but in latency-sensitive code (game frames, hot HTTP loops), the GC pause can matter.

collectgarbage() is the control surface:

collectgarbage("count")        -- KB used (with fraction)
collectgarbage("collect")      -- full collection (default)
collectgarbage("stop")         -- pause GC (allocations only)
collectgarbage("restart")      -- resume GC
collectgarbage("step", n)      -- run n KB of work, return true if cycle ended
collectgarbage("setpause", n)  -- new collection when heap grows by n%
collectgarbage("setstepmul", n)-- step size multiplier

Default tuning (Lua 5.3):

  • pause = 200 — start a new GC cycle when heap is 2x last collection
  • stepmul = 200 — collect 2x faster than allocation

Latency-tuned (more frequent, smaller pauses):

collectgarbage("setpause", 100)
collectgarbage("setstepmul", 100)

Makes GC do more work per allocation; smaller pauses but lower throughput.

Throughput-tuned (less frequent, longer pauses):

collectgarbage("setpause", 400)
collectgarbage("setstepmul", 500)

Good for batch processing. Bad for game loops.

Manual control for hot paths:

collectgarbage("stop")
for frame = 1, 1000 do
    update()
    render()
end
collectgarbage("restart")
collectgarbage("collect")    -- catch up after the loop

You prevent GC during the critical section. This works only if you're allocation-light — otherwise heap explodes.

Avoiding allocations (the real performance lever):

  • Pre-allocate buffers; reuse them.
  • Avoid string concatenation in loops (table.concat instead).
  • Avoid creating tables inside loops.
  • Use local references to globals (local print = print).
  • Avoid closures in hot paths (each closure allocates upvalues).
  • Reuse functions instead of creating fresh closures per call.

Memory profiling:

local before = collectgarbage("count")
run_my_code()
local after = collectgarbage("count")
print(string.format("used %.1f KB", after - before))

Generational mode (Lua 5.4):

collectgarbage("generational")    -- minor GCs cheap; major GCs less often
collectgarbage("incremental")     -- back to default

Generational mode is faster for code that allocates many short-lived objects (most code) but uses more memory.

Real-world:

  • Roblox: heavily customized GC for soft real-time
  • LÖVE / game engines: stop GC during frame updates, run between frames
  • OpenResty: GC tuning per-worker for HTTP latency

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…