Stopping Garbage Collection Spikes in Unity Mobile
Those random hitches on Android are often the garbage collector cleaning up after your code. Here is how to find the allocations, the usual culprits, and the small fixes that remove them.

Unity garbage collection spikes on mobile happen when your code allocates memory every frame and the collector stops to clean it up. To fix them, find per-frame allocations with the Profiler's GC Alloc column, remove the common culprits like string building and LINQ in Update, and make sure incremental GC is turned on.
What a GC spike looks like
The symptom is a hitch. The game runs smoothly, then freezes for a fraction of a second, seemingly at random. On low-end Android it can be bad enough that players notice every few seconds.
The cause is usually the same. Code creates small objects on the managed heap, those objects are thrown away, and eventually the garbage collector runs to reclaim the memory. When it runs, it takes time from your frame.
Step 1: turn on incremental GC
Incremental garbage collection splits the work across several frames instead of doing it all at once. It does not remove allocations, but it softens the spikes.
Check it under Project Settings > Player > Other Settings > Use incremental GC. It is enabled by default in recent Unity versions, but older projects that were upgraded may have it off.
Step 2: find the allocations
Open Window > Analysis > Profiler, connect to a development build on the phone, and select the CPU Usage module. In the Hierarchy view, sort by the GC Alloc column.
Anything that allocates every frame during normal play is a target. A few bytes per frame sounds small, but at 60 frames per second it adds up fast. The goal for steady gameplay is as close to zero per-frame allocation as you can get.
If you cannot tell where an allocation comes from, enable Deep Profile briefly, or turn on allocation call stacks in the Profiler toolbar. Both slow the game down, so use them to locate, then turn them off.
The usual culprits and their fixes
Most per-frame allocations come from a short list. Here are the ones we see most in mobile projects, with the fix for each.
| Culprit | Why it allocates | Fix |
|---|---|---|
| String building in Update | Every concatenation makes a new string | Update text only when the value changes |
| LINQ in hot paths | Creates enumerators and closures | Plain loops |
new WaitForSeconds in coroutines | New object every call | Cache it once |
RaycastAll and similar | Returns a new array | Use the NonAlloc versions |
gameObject.tag == "Enemy" | Reads the tag as a new string | CompareTag("Enemy") |
| Lambdas that capture variables | Allocate a closure each time | Cache the delegate or avoid the capture |
Text that updates every frame
Score and timer labels are a classic source. Update them only when the number changes, and use TextMeshPro's formatted SetText, which avoids building a new string.
int lastScore = -1;
void Update()
{
if (score != lastScore)
{
scoreLabel.SetText("{0}", score);
lastScore = score;
}
}
Coroutine waits
Creating a wait object inside a loop allocates every iteration. Cache it.
static readonly WaitForSeconds Tick = new WaitForSeconds(0.5f);
IEnumerator Pulse()
{
while (true)
{
DoPulse();
yield return Tick;
}
}
Physics queries
Use a reusable buffer instead of a fresh array.
readonly RaycastHit[] hits = new RaycastHit[16];
void Scan(Ray ray)
{
int count = Physics.RaycastNonAlloc(ray, hits, 50f);
for (int i = 0; i < count; i++) Handle(hits[i]);
}
Step 3: stop spawning and destroying
Instantiating and destroying objects during play creates garbage and other costs too. Bullets, particles, pickups, and floating text are the usual suspects.
Pool them instead. Create a set at load time and reuse them. Our guide on object pooling to fix mobile stutter walks through a simple pool.
Step 4: allocate at the right time
Some allocation is unavoidable. The trick is to move it to moments when a hitch does not matter.
- Load and build lists during loading screens, not mid-level.
- Pre-size collections with a capacity so they do not grow during play.
- Use the overloads that fill an existing list, such as
GetComponents<T>(List<T>). - Call
System.GC.Collect()on a loading screen if you want a clean heap before gameplay starts.
Allocation is not the enemy. Allocation during gameplay is. Move it to loading screens and menus.
There is also an advanced option, GarbageCollector.GCMode, which lets you disable collection during short gameplay sections. It is powerful but risky, because memory keeps growing until you turn it back on. Use it only after the basics are done, and test memory on your weakest phone.
Check UI too
Canvas rebuilds can allocate as well as cost CPU time. If your Profiler shows allocations under Canvas or layout calls, our post on Unity UI performance on mobile has the fixes.
What we would do
We would profile a two-minute play session on the slowest supported Android phone, sort by GC Alloc, and fix the top five entries. In most projects that alone removes the visible hitches.
Then we would add pooling for anything spawned during play and confirm incremental GC is on. Only if spikes remained would we look at controlling the collector directly.
The short version
- GC spikes come from per-frame allocations. The collector pauses to clean up.
- Turn on incremental GC to spread the work across frames.
- Sort the Profiler by GC Alloc on a real device and fix the top entries.
- Cache waits, use NonAlloc queries, use
CompareTag, and update text only on change. - Pool spawned objects and move unavoidable allocation to loading screens.
If you have chased hitches for a week and want another pair of eyes on the profile, send it over.
Related reading: Debug frame drops on Android fast, Unity mobile performance on low-end Android, Android device fragmentation guide
Got a game idea? We build it.
You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.
Share Your Game Idea →