System.Threading.Timer
.NET
multithreading
timer management
C# programming

Reliably stop System.Threading.Timer?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Stopping System.Threading.Timer reliably is a little more subtle than just disposing it. The timer callback runs on thread-pool threads, so a callback may already be queued or executing when you try to stop the timer. A reliable shutdown needs to prevent future ticks and account for any callback already in flight.

Stop Future Ticks First

The first step is usually to disable further scheduling:

csharp
timer.Change(Timeout.Infinite, Timeout.Infinite);

That tells the timer to stop firing again. It does not guarantee that a callback already queued on the thread pool has disappeared. That is why Change alone is not enough if you need a strong shutdown guarantee.

Dispose the Timer and Wait for Completion

If you need to know when the timer is truly done, use the Dispose(WaitHandle) overload:

csharp
1using System;
2using System.Threading;
3
4class Demo
5{
6    private Timer? _timer;
7    private readonly AutoResetEvent _disposed = new(false);
8
9    public void Start()
10    {
11        _timer = new Timer(Tick, null, 0, 1000);
12    }
13
14    public void Stop()
15    {
16        if (_timer == null)
17            return;
18
19        _timer.Change(Timeout.Infinite, Timeout.Infinite);
20        _timer.Dispose(_disposed);
21        _disposed.WaitOne();
22        _timer = null;
23    }
24
25    private void Tick(object? state)
26    {
27        Console.WriteLine("tick");
28    }
29}

This pattern waits until the timer has finished with all queued callbacks before continuing.

Protect the Callback Too

Even with proper disposal, it is often wise to guard the callback with a flag so a late-arriving invocation exits immediately.

csharp
1using System;
2using System.Threading;
3
4class SafeTimer
5{
6    private Timer? _timer;
7    private int _stopped;
8
9    public void Start()
10    {
11        _timer = new Timer(Tick, null, 0, 1000);
12    }
13
14    public void Stop()
15    {
16        Interlocked.Exchange(ref _stopped, 1);
17        _timer?.Change(Timeout.Infinite, Timeout.Infinite);
18        _timer?.Dispose();
19        _timer = null;
20    }
21
22    private void Tick(object? state)
23    {
24        if (Volatile.Read(ref _stopped) == 1)
25            return;
26
27        Console.WriteLine("safe tick");
28    }
29}

This extra check is useful when the callback touches shared resources that are being torn down during shutdown.

Avoid Re-Entrancy Problems

Another common issue is overlapping callbacks. If the callback takes longer than the timer period, the next tick can arrive before the previous one finishes. That makes shutdown and shared-state management harder.

One common technique is to use a one-shot timer and reschedule only after the callback finishes. Another option in newer .NET code is to consider PeriodicTimer, which has a different usage model and can be easier to reason about for async loops.

The key idea is that "reliably stop the timer" often really means "reliably stop future work and avoid overlap while shutting down."

Separate Stopping From Resource Cleanup

It is often helpful to think of shutdown in two phases:

  • first prevent any new timer work from being scheduled
  • then clean up resources only after outstanding work is known to be done

That separation makes the code easier to reason about and avoids hidden races where the timer appears stopped but a final callback still touches disposed state.

If the timer drives asynchronous polling logic rather than a tiny synchronous callback, consider whether PeriodicTimer or a dedicated hosted background loop would be simpler. Many timer shutdown bugs come from using System.Threading.Timer in scenarios where a more explicit control flow would be easier to stop cleanly.

Common Pitfalls

  • Calling Dispose() and assuming no callback can still be running.
  • Forgetting to disable future ticks with Change(Timeout.Infinite, Timeout.Infinite).
  • Tearing down shared resources while a callback may still be in flight.
  • Ignoring overlapping callback execution when the callback takes longer than the timer interval.
  • Using timer shutdown as a substitute for proper synchronization around shared state.

Summary

  • Reliable timer shutdown requires more than just calling Dispose().
  • First stop future ticks, then account for callbacks that may already be queued or running.
  • Use Dispose(WaitHandle) when you need to wait for full timer shutdown.
  • Guard the callback if shutdown can race with callback execution.
  • Also consider overlap and re-entrancy, because they often make timer shutdown problems harder than they first appear.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.