Garbage Collection Budgets for 60 FPS Games
Achieving a consistent 60 frames per second (FPS) requires rendering each frame in under 16.67 milliseconds, leaving virtually no margin for unexpected pauses caused by automatic memory management. Game engineers eliminate Garbage Collection (GC) spikes by treating heap allocation as a strictly regulated resource. This article explores how development teams establish zero-allocation runtime policies, use memory pooling, leverage value types, and integrate continuous profiling to prevent GC-induced frame drops.
The 16.67ms Frame Budget and GC Spikes
In a 60 FPS rendering loop, all game logic, physics updates, animation, rendering commands, and engine overhead must execute within 16.67 milliseconds. Traditional garbage collectors, such as those found in C# (Unity, Godot) or custom runtime environments, manage memory automatically by periodically freezing execution to identify and free unused objects (stop-the-world phases).
Even a minor GC pause of 5 to 10 milliseconds can push the total frame time past the 16.67ms threshold, resulting in a dropped frame, visual stutter, and unresponsive input. Because GC triggers are typically based on accumulated allocations rather than fixed timers, unmanaged allocations introduce non-deterministic performance drops.
The Zero-Allocation Per-Frame Rule
To guarantee smooth performance, most engineering teams enforce a "zero-allocation per frame" budget during active gameplay. While memory allocation is permitted during loading screens, level transitions, and initialization phases, the per-frame allocation limit for core systems (physics, AI, rendering, UI, audio) is set strictly to 0 bytes.
Engineers achieve this by:
- Pre-allocating Collections: Pre-sizing lists, dictionaries, and arrays during startup to prevent dynamic resizing at runtime.
- Avoiding Temporary Allocations: Eliminating string concatenations, closures, lambda expressions, and LINQ queries inside update loops, which implicitly generate short-lived heap objects.
- Preventing Boxing and Unboxing: Ensuring value types are not implicitly converted to reference types when passed into generic or object-based APIs.
Object Pooling Architecture
When dynamic game entities—such as projectiles, enemies, visual effects, and audio sources—must spawn and despawn continuously, engineers use object pools instead of allocating and destroying instances at runtime.
An object pool pre-instantiates a fixed number of objects into an inactive state at level load. When an object is needed, the system activates it from the pool, resets its state, and returns it to the pool upon deactivation. This shifts all allocation overhead to non-critical loading phases and keeps the runtime heap entirely static.
Favoring Value Types and Cache-Friendly Structures
Engineers reduce GC pressure by shifting memory from the managed heap to the execution stack or unmanaged memory regions.
- Structs over Classes: Using value types
(
structin C# or C++) ensures that data is stored directly on the stack or inline within containing arrays, bypassing the garbage collector entirely. - Native and Unmanaged Memory: Modern engines allow developers to bypass managed runtimes using native memory buffers, unmanaged arrays, or data-oriented design frameworks (such as Unity's DOTS/Burst Compiler). This provides direct control over memory layout and deallocation without GC overhead.
Strategic GC Invocation and Incremental Collection
When allocations cannot be completely avoided, engineers manage how and when the garbage collector runs rather than leaving it to the runtime's default heuristic.
- Explicit Invocations at Safe Times: Triggering a full garbage collection manually during scenes where frame drops are imperceptible, such as loading screens, pause menus, or cutscene transitions.
- Incremental and Generational GC: Configuring the collector to split its workload across multiple frames. Instead of performing a long, single-frame cleanup, an incremental collector performs a few microseconds of work per frame, staying well inside the 16.67ms budget.
Automated Profiling and CI Budget Gates
Maintaining an allocation budget across large development teams requires strict automated validation. Engineers use memory profilers to track heap allocations on a per-frame, per-subsystem basis.
Many studios integrate automated performance tests into their Continuous Integration (CI) pipelines. These tests run automated gameplay scenarios in headless builds; if any recurring update loop generates heap allocations above the configured budget (typically zero), the build fails automatically, preventing GC regressions from reaching the main codebase.