Why does a System.Timers.Timer survive GC but not System.Threading.Timer?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
The short answer is that System.Threading.Timer does not keep itself alive. If you create one and then drop every strong reference, the garbage collector is free to reclaim it even though the native timer queue still knows about the callback. System.Timers.Timer often appears to "survive" longer, but that is usually because user code or a component container is still holding a reference, not because the GC treats it as special.
Why System.Threading.Timer Gets Collected
System.Threading.Timer is a lightweight wrapper around timer infrastructure used by the runtime. The important detail is that unmanaged timer registration is not a managed GC root.
That means this code is unsafe:
Because timer is a local variable, it becomes unreachable after StartTimer() returns. Once that happens, the runtime is allowed to collect it and the callbacks may stop.
The fix is simple: keep a strong reference for as long as the timer should run.
Why System.Timers.Timer Often Looks Different
System.Timers.Timer is a higher-level wrapper that raises an Elapsed event. In real code it is frequently stored in a field, held by a component container, or attached to a long-lived object. Those normal usage patterns create strong references that keep it alive.
In this example, the Worker instance references the timer through the _timer field, so the timer stays reachable as long as the Worker stays reachable.
That is not a GC exception. It is ordinary object reachability.
Do Not Read Too Much Into the Event Model
A common misunderstanding is that the Elapsed event itself keeps the timer alive. The event subscription does not create a magical root. The timer holds a reference to the delegate target, not the other way around.
So this is still fragile if the timer itself is not rooted:
After StartTimer() returns, there may be no strong reference to timer. Whether it appears to keep firing for a while is an implementation detail and a timing accident, not behavior you should rely on.
The Practical Rule
Treat both timer types the same from a lifetime perspective:
- keep a strong reference while the timer should be active
- dispose the timer when you are done
- do not assume callbacks imply reachability
If you really must use a short-lived local reference in an edge case, GC.KeepAlive() can extend the lifetime to a precise point in the method.
Common Pitfalls
- Creating a
System.Threading.Timerin a local variable and assuming the callback keeps it alive. - Concluding that
System.Timers.Timeris GC-proof because it happened to survive in one test. - Forgetting to dispose timers, which can leave callbacks running longer than intended.
- Assuming event subscription creates a strong reference back to the timer itself.
- Relying on observed timing instead of explicit object lifetime management.
Summary
- '
System.Threading.Timercan be collected as soon as no strong reference remains.' - '
System.Timers.Timeroften survives only because user code usually stores it in a field or container.' - Event handlers do not automatically root the timer.
- Keep a strong reference to either timer type for as long as you need it.
- Dispose the timer explicitly when its work is finished.

