Memory cleanup in a managed runtime like .NET usually feels invisible. The garbage collector hums along, sweeping through heaps and reclaiming objects that have fallen out of scope. But there’s a less obvious side to this process—the finalizer queue. When objects with finalizers stack up faster than the runtime can clear them, you get a backlog. That backlog doesn’t just sit there; it warps memory usage, starves resources, and drags down performance in ways that are easy to overlook until they bite you.
This article picks apart the finalizer queue backlog: how it forms, how you can spot it, and what you need to do to stop it from undermining your application’s stability.

How Finalization Actually Works in .NET
When an object overrides Finalize()—or uses a C# destructor—the GC won’t reclaim its memory the moment it becomes unreachable. Instead, the object gets shuffled into a dedicated structure called the finalizer queue. A separate thread, the finalizer thread, plucks objects from this queue one at a time and runs their finalizers. Only afterward, in a later GC cycle, does the memory get freed.
So you’re looking at a two-stage reclamation: first the object is marked for finalization, then—after its finalizer has run—the memory can actually be collected. That separation is there for a reason. It lets cleanup logic (releasing native handles, for instance) execute safely outside the main GC pause. But it also creates a natural choke point. The finalizer queue runs on a single thread, and every object waiting for finalization sits on the heap, holding onto memory, until both stages are done.
The Structure of the Finalizer Queue
Inside the runtime, the finalizer queue is a linked list of objects that registered a finalizer. During a collection, the GC identifies dead objects with finalizers and adds them to a sub-list called the “freachable” queue—these are the ones actually ready to be finalized. The finalizer thread walks through that freachable list, calling finalizers in order. After execution, the object disappears from the freachable queue and becomes a normal candidate for collection.
But here’s the catch: the finalizer thread runs asynchronously. An object can idle in the freachable queue for an unpredictable stretch. If dead objects pour into the queue faster than the finalizer thread can handle them, the backlog builds. And that backlog isn’t just a counter—it’s live memory you can’t touch until each finalizer completes.

What Causes a Finalizer Queue Backlog
Backlogs don’t show up in applications with a handful of finalizable objects. They emerge when the volume—or the behavior—of finalizable objects overwhelms the single-threaded cleanup model.
High Allocation Rate of Finalizable Objects
Applications that constantly allocate types with finalizers—database connections, file streams, custom wrappers around native resources—can flood the queue. Every allocation that eventually becomes unreachable adds another entry. In high-throughput systems, even well-written finalizers can’t keep up if the allocation rate spikes beyond what a single thread can process.
Slow or Blocking Finalizers
One badly behaved finalizer can stall the entire queue. If a finalizer does lengthy I/O, grabs a lock, or calls into unmanaged code with high latency, it holds up every other entry behind it. Remember, the finalizer thread is shared across all finalizable objects in the process. A single slowpoke turns a modest allocation rate into a serious backlog.
Thread Starvation or Priority Issues
The finalizer thread normally runs at a high priority, but certain runtime configurations or host processes can interfere. If it gets starved of CPU time—maybe because of heavy user thread activity or aggressive thread pool tuning—the queue drain rate drops, and objects pile up.
Large Object Heap (LOH) Finalizers Without Compaction
Objects on the Large Object Heap aren’t compacted by default in most GC modes. When a finalizable object lives on the LOH, the delay in finalization can add to fragmentation. The backlog holds memory hostage and can make the heap’s fragmentation profile worse, leading to out-of-memory exceptions even when there’s plenty of total memory available.
Spotting a Finalizer Queue Backlog
You won’t catch a backlog by glancing at surface-level memory counters. Total managed heap size can look fine while a big chunk of memory is tied up in objects waiting for cleanup.
Performance Counters
Windows Performance Monitor has a counter under .NET CLR Memory: Finalization Survivors. This tracks objects that survived a collection only because they’re waiting for finalization. If that number keeps climbing or stays persistently high, you’ve got a backlog. Pair it with Promoted Finalization-Memory from Gen 0 to see how fast new finalizable objects are entering the queue.
Memory Dump Analysis
In a memory dump, reach for the SOS debugging extension command !finalizequeue. It dumps all objects in the freachable queue and those registered for finalization. Look for a large total count or types that dominate the queue. For example:
0:000> !finalizequeue
SyncBlocks to be cleaned up: 0
Free-Threaded Interfaces to be released: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 18 finalizable objects (000001e0b8101a30->000001e0b8101ac0)
generation 1 has 17 finalizable objects (000001e0b81019a8->000001e0b8101a30)
generation 2 has 214 finalizable objects (000001e0b8101300->000001e0b81019a8)
Ready for finalization 0 objects (000001e0b8101ac0->000001e0b8101ac0)
MT Count TotalSize Class Name
00007ff8e6a1c6a0 1 24 System.WeakReference
...
If “Ready for finalization” shows a zero but hundreds of objects are still registered in generation 2, the finalizer thread is processing them but can’t keep up with the influx. Use !threads to check the finalizer thread’s state—see if it’s actually running or sitting there blocked.
ETW Tracing
Event Tracing for Windows providers like Microsoft-Windows-DotNETRuntime emit events for finalization activity. The GarbageCollection/FinalizeObject event fires for each finalizer execution. Analyzing timestamps and object counts over time lets you measure queue depth and processing latency directly.

Performance Fallout from a Backlog
A finalizer queue backlog isn’t just a memory leak. It kicks off a cascade of trouble that can make your whole application wobble.
Increased Memory Pressure and GC Frequency
As finalizable objects pile up, the managed heap grows. The GC responds by triggering more frequent collections—especially expensive generation 2 sweeps that scan the entire heap. You end up in a nasty cycle: high allocation, delayed finalization, aggressive GC, and a lot of CPU cycles burned for very little reclaimed memory. Some engineers call this a “mid-life crisis” for objects on the heap.
Gen 2 Heap Fragmentation
Objects that survive into generation 2 because of pending finalization can leave holes when they finally get released. In workloads without compaction, freed memory turns into gaps that are too small for new allocations. The heap size creeps up, and you might face an OutOfMemoryException.
Resource Leaks and Handle Exhaustion
Finalizers often release native resources—file handles, sockets, database connections. If the backlog delays finalization, those handles stay open far longer than you’d expect. In server applications, this can blow through the process’s handle limit, causing failures when opening new connections or files. You’ll see “Too many open files” errors or socket exceptions, even though your higher-level wrappers are being disposed correctly.
Application Pauses and Latency Spikes
When the GC does a blocking generation 2 collection to claw back memory, application threads are suspended. A backlog-driven spike in Gen 2 collections means more frequent—and longer—pauses. For latency-sensitive applications, that’s a fast way to miss your SLAs and annoy users.
How to Mitigate Finalizer Queue Backlogs
Preventing backlogs takes a mix of careful design choices and operational monitoring. There isn’t one magic switch.
Reduce Finalizable Object Count
The bluntest fix: don’t use finalizers unless you truly have to. Modern .NET gives you patterns like SafeHandle and IAsyncDisposable that sidestep finalization. When you wrap native resources, reach for SafeHandle instead of a raw IntPtr with a finalizer. The runtime handles SafeHandle finalization more efficiently and ties into the critical finalizer infrastructure.
Dispose Promptly and Deterministically
Use IDisposable and using statements with discipline. When you explicitly dispose an object, you can suppress its finalizer with GC.SuppressFinalize(this). That removes the object from the finalizer queue entirely—no two-phase delay. Audit your code paths to make sure every IDisposable gets disposed, even when exceptions fly.
Keep Finalizers Fast and Non-Blocking
If you absolutely can’t avoid a finalizer, strip its work down to the bare minimum. No I/O, no lock acquisition, no calls to external services. A common pattern: set a flag inside the finalizer that says “I’m done,” then hand off the real cleanup to a background thread or a dedicated resource-reclaim pool. The finalizer itself should finish in microseconds.
Monitor and Alert on Backlog Indicators
Instrument your application to track the Finalization Survivors performance counter. Set thresholds that trigger alerts when the count drifts above a baseline that makes sense for your workload. In production, periodic memory dumps can be analyzed offline to confirm whether the queue is growing.
Consider GC Mode Tuning
For server applications, switching to sustained low latency mode or workstation GC with background finalization might help the finalizer thread keep pace. But tread carefully—tuning GC modes changes memory management behavior across the board, so validate under realistic load before you commit.
FAQ
What’s the difference between the finalizer queue and the freachable queue?
The finalizer queue holds every object that registered a finalizer, reachable or not. The freachable queue is a subset—only objects the GC has marked unreachable and that are now waiting for their finalizers to run. After finalization, they leave the freachable queue and become eligible for normal collection.
Can a finalizer queue backlog cause an OutOfMemoryException even if the heap isn’t full?
Absolutely. The backlog can fragment the heap—especially the Large Object Heap or Gen 2—so free memory exists but no single block is big enough for an allocation. Also, if finalizers are holding up the release of native handles, the process can hit handle limits long before managed memory runs out.
How can I tell if a specific type is causing the backlog?
In a memory dump, !finalizequeue groups objects by method table (MT) and shows count and total size per type. Look for types with unusually high counts. Cross-reference with !dumpheap -stat to see the full heap picture. ETW events give you timestamps, so you can tie finalization delays to specific type names.
Is it safe to call GC.Collect() to force finalization?
Calling GC.Collect() followed by GC.WaitForPendingFinalizers() can drain the queue temporarily, but it’s not a production fix. It introduces blocking pauses and doesn’t touch the root cause. Keep it for testing or diagnostics, and focus on cutting finalizable allocations or tightening disposal patterns for a permanent solution.