A timer runs code after a delay or on a repeating interval: refresh a cache every minute, save a draft every 30 seconds, poll a service, update a clock. .NET has several timer classes that look alike but differ in where the callback runs and how you control them. This page covers each, plus Stopwatch for measuring how long something took.
Which timer to use
| Timer | Callback runs on | Style | Use for |
|---|---|---|---|
System.Threading.Timer | Thread pool | Callback delegate | Background work in services and libraries |
System.Timers.Timer | Thread pool (by default) | Elapsed event, Start/Stop | Component-style code that likes events |
PeriodicTimer (.NET 6+) | Wherever your async loop runs | await WaitForNextTickAsync() | Async code; never overlaps ticks |
System.Windows.Forms.Timer, WPF DispatcherTimer | The UI thread | Tick event | Updating controls in desktop apps |
The first two fire on thread pool threads, so their callbacks can run at the same time as the rest of your program, and even at the same time as each other. The UI timers run on the UI thread, which is why they can touch controls directly, and why a slow handler freezes the window.
System.Threading.Timer
The constructor takes the callback, a state object, the delay before the first tick, and the period. It starts immediately.
Example output:
tick 1 at ~200 ms
tick 2 at ~500 ms
tick 3 at ~800 ms
tick 4 at ~1100 ms
stopped after 4 ticks
Two details matter here. Main has to wait (done.WaitOne()); a console program exits when Main returns, and timer callbacks run on background threads that do not keep the process alive. And the timer lives in a static field: a System.Threading.Timer that nothing references can be garbage collected, and then it silently stops firing. This bites most often when a timer is created as a local variable inside a method that returns.
Change(dueTime, period) reschedules the timer; Timeout.Infinite for both pauses it, and a new pair restarts it. Dispose() stops it for good.
System.Timers.Timer
System.Timers.Timer wraps the same mechanism in an event-based API: set Interval, subscribe to Elapsed, then Start() and Stop().
Example output:
autosave #1
autosave #2
autosave #3
autosave stopped
AutoReset = false makes a one-shot timer: it fires once and stops, and calling Start() again schedules the next single tick. Enabled = true and false are the same as Start() and Stop(). The full name System.Timers.Timer is written out here because System.Threading also has a Timer; with both namespaces imported, a bare Timer is ambiguous and does not compile.
System.Timers.Timer also has a SynchronizingObject property that marshals Elapsed onto a UI thread in WinForms. In practice the UI framework's own timer is simpler for that.
Callbacks overlap and run on other threads
Both classes above fire on the thread pool, on schedule, whether or not the previous callback has finished. If a callback takes 3 seconds and the period is 1 second, three callbacks run at once. That creates two obligations:
- Anything the callback touches must be thread safe. Use
Interlockedfor counters (as the examples do) and a lock for anything larger. - Guard against overlap when it matters. Either skip a tick while the previous one is still running, or use a pattern that cannot overlap.
A skip guard with Interlocked:
private int running = 0;
private void OnTick(object state)
{
if (Interlocked.Exchange(ref running, 1) == 1) return; // previous tick still busy
try
{
SyncOrders(); // slow work
}
finally
{
Volatile.Write(ref running, 0);
}
}
Exceptions are the other trap. An exception that escapes a System.Threading.Timer callback crashes the process, while System.Timers.Timer swallows exceptions from Elapsed handlers so the failure goes unnoticed. Wrap the body of a timer callback in try/catch and log.
PeriodicTimer and async loops
In async code, a loop that waits between iterations is the clearest timer: the next wait starts only after the work finishes, so ticks can never overlap, and exceptions surface where you can catch them. .NET 6 added PeriodicTimer for exactly this:
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await SaveDraftAsync();
}
WaitForNextTickAsync returns false once the timer is disposed, which ends the loop, and throws OperationCanceledException when the token is cancelled. Unlike a loop around Task.Delay, a PeriodicTimer keeps a fixed rhythm: if the work takes 2 seconds, the next tick still comes 30 seconds after the previous tick, not 32.
On any version, the same shape with Task.Delay works when drift does not matter:
Example output:
poll 1
poll 2
poll 3
poll 4
polling cancelled after 4 rounds
Cancellation replaces Stop(): pass a token into the loop and cancel it from outside. See tasks for more on cancellation tokens.
Measuring elapsed time with Stopwatch
A timer schedules code; Stopwatch measures how long code took. It reads a high-resolution clock that only moves forward, which makes it the right tool for timing:
Example output:
string += 64 ms
StringBuilder 0 ms
Same length: True
High resolution clock: True
Start, Stop, Reset and Restart control it; Elapsed is a TimeSpan, and ElapsedMilliseconds and ElapsedTicks give raw numbers. Avoid timing with DateTime.Now: its resolution can be as coarse as 10 to 15 ms, and it jumps when the system clock is adjusted. For serious benchmarks, where JIT warm-up and garbage collection distort a single run, use the BenchmarkDotNet library.
Timer accuracy
A timer interval is a minimum, not a promise. On Windows the default system timer resolution is about 15.6 ms, so a 10 ms interval tends to fire every 15 or 16 ms, and on a busy machine a thread pool callback can start later still. Timers are right for "about every N seconds". For precise frame timing or media, use APIs built for that.
Common mistakes
- A
System.Threading.Timerstored only in a local variable. It can be garbage collected and stop firing; keep it in a field. - Letting
Mainreturn. The process ends and takes the timer with it. - Ambiguous
Timer. WithSystem.ThreadingandSystem.Timersboth imported, write the full name. - Assuming ticks never overlap. Thread pool timers overlap when the work is slower than the interval.
- Touching UI controls from a thread pool timer. Use the UI framework's timer or marshal back to the UI thread.
- Unhandled exceptions in callbacks. They crash the process (
Threading.Timer) or vanish (Timers.Timer); catch and log. - Forgetting
Dispose. Timers hold system resources and keep firing until disposed.
Frequently Asked Questions
Which timer should I use in C#?
In async code on .NET 6 or later, PeriodicTimer with await timer.WaitForNextTickAsync() in a loop. For a callback on the thread pool, System.Threading.Timer. For an event-based timer with Start, Stop and an Elapsed event, System.Timers.Timer. In WinForms or WPF, use the UI framework's timer so the handler runs on the UI thread.
How do I run code every few seconds in C#?
Create a timer with the interval, for example new System.Threading.Timer(_ => Refresh(), null, 0, 5000) to call Refresh now and every 5 seconds, and keep a reference to it. In async code, loop with PeriodicTimer (.NET 6+) or await Task.Delay(5000); the loop version never runs two ticks at once.
How do I stop a timer in C#?
For System.Timers.Timer, call Stop() (or set Enabled = false), and Dispose() when you no longer need it. For System.Threading.Timer, call Change(Timeout.Infinite, Timeout.Infinite) to pause it, or Dispose() to stop it for good. A callback that already started may still finish after you stop the timer.
Why does my System.Threading.Timer stop firing?
Nothing references the timer, so the garbage collector collected it and its callbacks stopped. This happens when the timer is created as a local variable in a method that returns. Store it in a field for as long as it should keep running.
How do I measure elapsed time in C#?
Use System.Diagnostics.Stopwatch: var sw = Stopwatch.StartNew(); ... sw.Stop(); then read sw.ElapsedMilliseconds or sw.Elapsed. It uses a high-resolution monotonic clock, unlike subtracting two DateTime.Now values, which has coarser resolution and jumps when the system clock is adjusted.