If you have been coding in C# for a little while, you have likely run into a situation where you need to run a piece of code repeatedly—maybe polling an API, refreshing a cache, or sending a heartbeat signal.
You type “Timer” into your IDE, and suddenly you are bombarded with options:
System.Threading.Timer
System.Timers.Timer
PeriodicTimer
It can be overwhelming. Why are there so many? Which one should you pick?
In this guide, we will demystify these three common timers, look at how they work under the hood, and learn how to implement the modern standard using PeriodicTimer with graceful cancellation.
1. System.Threading.Timer: The Lightweight Workhorse
Think of System.Threading.Timer as the “bare metal” option. It has been around since the very beginning of .NET.
How it works
This is a simple timer that executes a callback method on a Thread Pool thread. When the timer expires, the system grabs a thread from the pool and runs your code.
using System.Threading;
public class LightweightExample
{
public void Start()
{
// Create a timer that waits 1 second, then repeats every 2 seconds
var timer = new Timer(CallbackMethod, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
Console.WriteLine("Press Enter to stop...");
Console.ReadLine();
// Clean up!
timer.Dispose();
}
private void CallbackMethod(object? state)
{
Console.WriteLine($"Task executed on thread: {Thread.CurrentThread.ManagedThreadId}");
}
}
The “Gotcha”: Re-entrancy
This is crucial for beginners. If your timer is set to tick every 1 second, but your code takes 3 seconds to run, the timer won’t wait. It will trigger again on a different thread while the first one is still running.
Pros: Extremely lightweight and efficient.
Cons: Harder to use (requires callback delegates), doesn’t handle async code well, and you must manually handle overlapping executions.
2. System.Timers.Timer: The Event-Based Wrapper
This is the timer most developers stumble upon first because it feels very familiar. It wraps System.Threading.Timer but adds a layer of “quality of life” features.
How it works
Instead of dealing with callbacks, this timer fires an Elapsed event. It’s easier to read but suffers from similar issues as the threading timer.
using System.Timers;
public class EventTimerExample
{
public void Start()
{
var timer = new Timer(2000); // 2 seconds
// Hook up the event
timer.Elapsed += OnTimedEvent;
timer.AutoReset = true; // Keep repeating
timer.Enabled = true; // Start the timer
Console.WriteLine("Press Enter to exit...");
Console.ReadLine();
}
private void OnTimedEvent(Object source, ElapsedEventArgs e)
{
Console.WriteLine($"The Event was raised at {e.SignalTime}");
}
}
Pros: Easy to read syntax (Events), easy to start/stop.
Cons: Still suffers from re-entrancy (overlapping) issues, and exception handling inside events can be tricky.
3. PeriodicTimer: The Modern Hero (.NET 6+)
Introduced in .NET 6, PeriodicTimer was designed specifically to solve the problems of the previous two timers, particularly regarding Async/Await and Re-entrancy.
How it works
Instead of firing an event or a callback “blindly,” PeriodicTimer is designed to be used inside a loop. It waits for you to finish your work before ticking again.
public async Task RunPeriodicAsync()
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
// This loop runs as long as the timer is ticking
while (await timer.WaitForNextTickAsync())
{
try
{
Console.WriteLine($"Doing work at {DateTime.Now}");
// Simulating work that takes time (e.g., calling an API)
await Task.Delay(1000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
The “Gotcha”: It waits for you!
Notice the await timer.WaitForNextTickAsync(). This timer will not tick again until your code inside the loop finishes and loops back around. This effectively eliminates overlapping executions without you writing any extra code.
Level Up: Handling “Graceful Cancellation”
In real-world applications (like a web server or a background service), you rarely want to just “kill” the program. You want to signal it to stop so it can finish its current task and shut down cleanly.
With PeriodicTimer, this is surprisingly easy because it supports CancellationToken natively.
Here is the complete, industry-standard pattern:
using System.Diagnostics;
public class GracefulShutdownExample
{
public static async Task Main(string[] args)
{
// 1. Create the "Stop Button" (Source)
using var cts = new CancellationTokenSource();
Console.WriteLine("Timer started. Press 'C' to cancel/stop...");
// Start the timer task in the background
Task timerTask = RunTimerAsync(cts.Token);
// Wait for user input
while (Console.ReadKey(true).Key != ConsoleKey.C) { }
Console.WriteLine("\nC pressed! Cancelling...");
// 2. Press the stop button
cts.Cancel();
// Wait for the timer task to finish up gracefully
try
{
await timerTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("Timer stopped successfully.");
}
}
public static async Task RunTimerAsync(CancellationToken token)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
try
{
// 3. Pass the token into WaitForNextTickAsync
while (await timer.WaitForNextTickAsync(token))
{
Console.WriteLine($"[Tick] executed at {DateTime.Now:HH:mm:ss}");
// Simulate work... pass the token here too!
await Task.Delay(500, token);
}
}
catch (OperationCanceledException)
{
// This block runs when cts.Cancel() is called
Console.WriteLine(">> Shutdown signal received. Cleaning up resources...");
}
}
}
Why is this better?
Immediate Response: If the timer is waiting for a long interval (e.g., 10 minutes), cancelling the token wakes it up immediately. You don’t have to wait for the 10 minutes to finish.
Safety: The try/catch block gives you a specific place to close database connections or save files before the thread dies.
Summary Comparison
Here is a quick reference to help you spot the differences:
| Feature | System.Threading.Timer | System.Timers.Timer | PeriodicTimer |
|---|
| Style | Callback (Delegate) | Event-Based | Async Loop |
| Re-entrancy | Yes (Runs overlap) | Yes (Runs overlap) | No (Waits for previous to finish) |
| Async Support | Poor | Poor | Excellent |
| Best For | High-performance / Legacy | Simple background tasks | Modern Async Tasks |
Final Recommendation
Here is my decision tree for 2024 and beyond:
Are you using .NET 6 or newer?
Are you on an older .NET Framework?
Use System.Timers.Timer if you want simple, easy-to-read code.
Use System.Threading.Timer only if you are building a high-performance library and need to save every ounce of memory.
Happy Coding!