Garbage Collection in Lua
collectgarbage is the Lua function you use to control the garbage collector. It is not complicated, but most people use it wrong because they treat it like a magic button instead of understanding what the collector is actually doing. The garbage collector runs automatically, but it does not run on a timer. It runs based on allocation thresholds. When you allocate enough objects, the collector kicks in and reclaims memory. collectgarbage gives you a handful of commands to interact with that process. The most common call is collectgarbage("count"), which returns the total memory in use on the Lua stack in kilobytes. You can also call collectgarbage("stop") to freeze the collector entirely, or collectgarbage("restart") to start it again. collectgarbage("step") runs a single incremental step, which is the most useful command for manual memory management. collectgarbage("setpause", value) and collectgarbage("setstepmul", value) let you tune how aggressive the collector is.
How It Actually Works In Practice
Here is the thing nobody tells beginners about collectgarbage: it is not your friend if you are doing heavy object creation. When you call collectgarbage("collect"), it runs a full garbage collection cycle, and during that cycle the entire world pauses. In standard Lua 5.3+, a full stop-the-world collection on a large heap can easily cause a frame spike of 50 to 200 milliseconds depending on how much garbage has accumulated. In LuaJIT, it is even worse because LuaJIT's GC is generational and the full sweep hits the stable generation, which can be significantly slower than you expect. I worked on a project where we were simulating thousands of particle objects every frame. The default collector settings caused visible stutter on lower-end hardware. My fix was not to call collectgarbage more often. It was the opposite. I switched to incremental collection mode by calling collectgarbage("setpause", 200) and collectgarbage("setstepmul", 50). This made the collector run in smaller, more frequent steps instead of pausing the thread for one big sweep. The peak memory usage went up slightly, but frame time became consistent. The tradeoff is worth it in almost every case where frame consistency matters more than squeezing out every last byte.
Common Mistakes
The first mistake is calling collectgarbage("collect") every frame. This is almost always the wrong answer. It creates unnecessary work for the collector and destroys your frame times. If you are doing this, stop immediately. The collector is designed to run autonomously and will handle most situations correctly without intervention. The second mistake is confusing collectgarbage("count") with actual memory. The value returned is approximate. It includes Lua-managed memory but excludes native allocations, C-side buffers, and any memory outside the Lua VM. If you are working in Roblox, collectgarbage("count") only reports the Lua heap, not the total game memory. Roblox has its own engine-level memory manager that collectgarbage cannot see or control. The third mistake is not understanding that collectgarbage("step") does not guarantee completion. A single step may reclaim some objects and then stop. You need to call it repeatedly in a loop until it returns true, which indicates the collector has finished a cycle. Here is a reliable pattern for draining the collector gradually:
local done = false while not done do done = collectgarbage("step") end This loop will keep running steps until the collection finishes. It is useful when you need to reclaim memory before a specific event, like loading a new level.
When collectgarbage Is Not Enough
There are scenarios where manual collection control simply cannot solve your memory problem. If your code has a genuine memory leak, no amount of tweaking collectgarbage settings will fix it. A leak happens when references are retained unintentionally, usually through global tables, event handlers that are never unregistered, or caches that grow without bounds. In those cases, the correct approach is profiling and finding the leak, not calling collectgarbage more aggressively. In Roblox specifically, collectgarbage has very limited utility. The engine runs its own memory manager, and using collectgarbage for memory troubleshooting in a Roblox game will mostly give you misleading information. If you are working in Roblox and hitting memory issues, you should be using the built-in Profile Service or Roblox Studio's built-in memory profiler instead of relying on Lua's garbage collector commands.
Quick Reference
collectgarbage("count") returns current memory in KB. collectgarbage("collect") runs a full cycle and pauses execution. collectgarbage("step") runs one incremental step. collectgarbage("setpause", N) sets the pause threshold as a percentage. collectgarbage("setstepmul", N) sets the step multiplier as a percentage. collectgarbage("stop") halts the collector. collectgarbage("restart") resumes it. The default pause value in Lua 5.4 is 200, which means the collector waits until memory doubles before starting a new cycle. The default step multiplier is 200, which makes each step twice as fast as it would be otherwise. If your application is creating objects rapidly, increasing these values slightly can reduce GC pressure at the cost of slightly higher peak memory. If your application is memory-constrained and does not create many objects, lowering them may help, but you will pay for it in more frequent collection pauses. Most projects should leave collectgarbage alone. Only reach for it when you have measured a real problem and identified that the garbage collector is the bottleneck.