Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614479)
- First published
Cite this article(DOI: 10.5281/zenodo.21614478)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Choosing Between .NET's Three Timers - PeriodicTimer/Timer/DispatcherTimer. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614478 https://comcomponent.com/en/blog/2026/03/12/002-periodictimer-system-threading-timer-dispatchertimer-guide/
- DOI (latest version)
- 10.5281/zenodo.21614478
- DOI (this version)
- 10.5281/zenodo.22217133
In the previous article, A Practical Guide to Getting as Close to Soft Real-Time as Possible on Ordinary Windows - The Checklist to Look at First, we covered avoiding Sleep-driven periodic loops in favor of event-driven approaches and waitable timers. In one line, the conclusion was that if you want to reduce period jitter and deadline misses, design how you wait before you pick a timer type.
So what do you do in more everyday .NET app development?
The place where people get lost is PeriodicTimer, System.Threading.Timer, and DispatcherTimer.
They are all called timers, but
- a timer whose ticks you
await - a timer whose callbacks arrive on the ThreadPool
- a timer that runs on the UI thread’s
Dispatcher
have quite different characters.
flowchart TB
accTitle: Three timers that share a name but not a character
accDescr: Shows how the three differ in character: PeriodicTimer waits for ticks with await, System.Threading.Timer delivers callbacks on the ThreadPool, and DispatcherTimer runs on the UI thread.
t["Three timers"] --> pt["PeriodicTimer: wait with await"]
t --> st["Timer: callbacks arrive"]
t --> dt["DispatcherTimer: UI thread"]
Figure 1: They are all called timers, but how you wait and where they run are completely different.
What tends to get mixed up in practice is roughly this.
- Passing an
asynclambda toSystem.Threading.Timereven though the periodic work is asynchronous - Touching the screen directly from a ThreadPool timer even though it is a WPF UI update
- Putting heavy work in a
DispatcherTimerand slowing down the whole screen - The previous soft real-time discussion and ordinary app periodic execution blurring together in your head
This article assumes mainly general C# / .NET apps on .NET 6 or later, and organizes
PeriodicTimer / System.Threading.Timer / DispatcherTimer in an order that makes everyday practice less confusing.
The intended targets are these.
- Workers / background services
- Console apps
- Behind-the-scenes processing in ASP.NET Core
- WPF desktop apps
By DispatcherTimer, this article mainly means WPF’s System.Windows.Threading.DispatcherTimer.
WinUI / UWP have a DispatcherTimer built on the same idea.
For WinForms, it is more natural to look at System.Windows.Forms.Timer as the UI timer.
This article leans on WPF for the UI side, but WinForms is in scope too. Read DispatcherTimer as System.Windows.Forms.Timer and the cautions in 4.3 and 5.2 apply as they stand. On top of that, only these two points differ.
System.Windows.Forms.Timeris a single-threaded timer whose Tick is raised through the message loop, and it has no priority setting (DispatcherPriority) likeDispatcherTimerdoes- Microsoft’s documentation states that its accuracy is limited to about 55 milliseconds. It is not suited to fine-grained intervals, so in that case look at something other than a UI timer
Note that what we deal with here is how to write periodic execution on the app side. When the accuracy of the period itself is the subject, we return to the discussion in the previous soft real-time article.
flowchart TB
accTitle: What this article covers
accDescr: Shows the split: how to write periodic execution on the app side is this article's scope, and when the accuracy of the period itself is the subject the discussion returns to the soft real-time article about designing how you wait.
q{"What is the subject"}
q -->|"How to write app-level periodic execution"| here["The three timers in this article"]
q -->|"The accuracy of the period itself"| rt["The previous article on designing how you wait"]
Figure 2: Even under the same phrase do something at a fixed interval, writing periodic execution and designing period accuracy are separate problems.
Also, the code appearing in this article is published on GitHub as a complete buildable and runnable sample set (libraries and console demos for PeriodicTimer / System.Threading.Timer, plus unit tests that verify tick coalescing and callback overlap).
periodictimer-system-threading-timer-dispatchertimer-guide - komurasoft-blog-samples (GitHub)
Table of Contents
- The Conclusion First (In One Line)
- The One-Sheet Overview
- 2.1. The Big Picture
- 2.2. The First-Pass Decision Table
- What to Distinguish First
- 3.1. Callback Style, or Awaiting Ticks?
- 3.2. Does It Run on the ThreadPool, or on the UI Thread?
- 3.3. Periodic Processing and Precision Guarantees Are Separate Topics
- Typical Patterns
- 4.1. For Async Periodic Work:
PeriodicTimer - 4.2. For Light Callbacks on the ThreadPool:
System.Threading.Timer - 4.3. For WPF UI Updates:
DispatcherTimer - 4.4. For Soft-Real-Time-Leaning Periodic Work: Look at Other Tools
- 4.1. For Async Periodic Work:
- Common Anti-Patterns
- Code Review Checklist
- A Rough Decision Guide
- Summary
- References
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (18 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
1. The Conclusion First (In One Line)
- If you want to write fixed-interval work naturally on an
awaitbasis, start withPeriodicTimer - If you want to fire light callbacks periodically on the ThreadPool, use
System.Threading.Timer - If you want to update the screen on WPF’s UI thread, use
DispatcherTimer System.Threading.Timercallbacks can overlap. Cramming async work in carelessly gets messy fastDispatcherTimerlets you touch the UI directly, but in exchange, heavy work in it easily stalls the UI itself- In the soft real-time context of the previous article, these three are not the main tool for high-precision waiting
In short, the first three things to look at are these.
- On which thread / context do you want it to run?
- Do you want to write the body sequentially with
async/await? - Can you tolerate overlapping callbacks?
Just separating these three makes things much less confusing.
flowchart TB
accTitle: The three questions to ask first
accDescr: Just separating three questions - which thread or context it should run on, whether you want to write the body sequentially with async and await, and whether overlapping callbacks are acceptable - makes the choice much less confusing.
q1["Where should it run"] --> pick["The timer choice falls out"]
q2["Write it sequentially with async"] --> pick
q3["Can overlapping callbacks be tolerated"] --> pick
Figure 3: Before the timer names, separate these three questions.
2. The One-Sheet Overview
2.1. The Big Picture
flowchart LR
A["Want to do something at a fixed interval"] --> B{"Run it on the UI thread?"}
B -- "Yes" --> C["DispatcherTimer"]
B -- "No" --> D{"Write the body<br/>plainly with<br/>async / await?"}
D -- "Yes" --> E["PeriodicTimer"]
D -- "No" --> F{"Run light callbacks<br/>on the<br/>ThreadPool?"}
F -- "Yes" --> G["System.Threading.Timer"]
F -- "No" --> H["Consider another design<br/>Channel / BackgroundService / event / waitable timer"]
Figure 4: Cut in the order UI thread, then async, then light callback, and the three timers fall out.
In practice, this branching is mostly sufficient. When in doubt, the least error-prone cut to make first is
PeriodicTimer for async work, DispatcherTimer for UI updates.
System.Threading.Timer is handy, but with its callback overlap and lifetime-management quirks,
it is a bit temperamental as your very first choice.
2.2. The First-Pass Decision Table
| Situation | First choice | Where it runs | Why it fits | First caution |
|---|---|---|---|---|
| Run async work like HTTP / DB / file I/O at a fixed interval | PeriodicTimer |
Within the flow of your current async method | You can write it on an await basis, and stopping and cancellation are straightforward |
One timer, one consumer. Delays are not parallelized automatically |
| Run a light heartbeat / metrics emission / cache-expiry check on the ThreadPool | System.Threading.Timer |
ThreadPool | Lightweight, callback-style. Easy to fit into existing callback-based designs | Callbacks assume reentrancy. They can overlap. Hold a reference |
| Run a WPF clock display or light UI updates at a fixed interval | DispatcherTimer |
WPF’s Dispatcher (UI thread) |
Can touch the UI directly. Has priorities | Exact firing times are not guaranteed. Heavy work clogs the UI |
The accuracy of the period is the point, and you want to avoid Sleep-driven loops |
Do not make these three your main tool | - | The goal is not app-level periodic execution but designing wait precision | Look at events / waitable timers instead |
What matters in this table is to look at the execution location and the writing style, not the timer’s name. When timer selection goes wrong, the cause is more often not looking at where it runs than the API name.
flowchart TB
accTitle: What to look at when choosing a timer
accDescr: Timer selection goes wrong when the choice is made from the API name; looking at where the code runs and how you want to write it makes the choice hard to get wrong.
name["Choose by name"] --> miss["Miss where it runs and it goes wrong"]
look["Choose by execution location and writing style"] --> hit["A choice that is hard to get wrong"]
Figure 5: Most mistakes come from choosing by name. What to look at is where it runs and how you want to write it.
3. What to Distinguish First
3.1. Callback Style, or Awaiting Ticks?
Separating this immediately improves visibility.
System.Threading.TimerandDispatcherTimerare callback / event stylePeriodicTimeris the style where youawaitthe next tick
That is,
- callback style means the timer calls you
PeriodicTimermeans you wait for the next tick
and that is the difference.
If the body is async and
you want to read wait, process, wait again as one continuous flow, PeriodicTimer is more natural.
Conversely, when
- you want to fit into an existing callback-based design
- the body is short and synchronous
- you simply want a periodic kick
then System.Threading.Timer fits.
PeriodicTimer is convenient but not all-purpose.
It is not designed for multiple concurrent WaitForNextTickAsync calls on one timer,
and if multiple ticks occur while you are not waiting, they are coalesced into one.
It is important not to misread this as it catches up automatically.
flowchart TB
accTitle: Callback style versus awaiting ticks
accDescr: Shows the difference: System.Threading.Timer and DispatcherTimer are callback style where the timer calls you, while PeriodicTimer is the style where you await the next tick yourself.
q{"Which style"}
q -->|"Callback style"| cb["The timer calls you"]
q -->|"Await-the-tick style"| tick["You wait for the next tick"]
cb --> cbex["Timer and DispatcherTimer"]
tick --> ptex["PeriodicTimer"]
tick -.-> flow["Wait, process, wait again in one flow"]
Figure 6: Called, or waiting. This distinction alone clears up the view considerably.
3.2. Does It Run on the ThreadPool, or on the UI Thread?
The next thing to look at is where it executes.
System.Threading.Timer callbacks run on the ThreadPool, not on the creating thread.
This makes it suitable for background work, but it is not meant to touch the UI directly.
DispatcherTimer, by contrast, is a UI timer integrated into the Dispatcher queue.
In WPF, it runs on the same Dispatcher, so you can update the UI directly inside the Tick handler.
This difference is quite large.
- To touch the UI from a ThreadPool timer, you must explicitly marshal back to the UI
DispatcherTimermakes the UI easy to touch, but correspondingly consumes UI-thread time
In other words, DispatcherTimer’s strength is that it can touch the UI safely, but
that simultaneously means heavy work in it drags down input and rendering too.
flowchart TB
accTitle: The trade-off behind where the timer runs
accDescr: System.Threading.Timer callbacks run on the ThreadPool, so touching the UI requires marshaling back explicitly, while DispatcherTimer can touch the UI directly but spends UI thread time to do it.
st["Timer: runs on the ThreadPool"] --> back["Marshal back explicitly to touch the UI"]
dt["DispatcherTimer: UI thread"] --> easy["Touch the UI directly"]
easy --> cost["Heavy work drags input and rendering along"]
Figure 7: The difference in where the code runs is a trade between easy UI access and spent UI-thread time.
3.3. Periodic Processing and Precision Guarantees Are Separate Topics
This part matters as the connection to the previous article.
Even though the phrasing do something at a fixed interval is the same,
- wanting periodic work every few seconds as an app-level convenience
- wanting to get as close to a deadline as possible at the 1 ms to few-ms scale
are different problems.
System.Threading.Timer is a lightweight, easy-to-handle timer,
but it is not a dedicated tool for precision.
DispatcherTimer, too, is affected by the state of the Dispatcher queue and by priorities.
PeriodicTimer may look, from its name alone, like the period must be tight,
but its practical strength is how easy the async flow is to write, not precision.
So it is safer to separate at the outset whether you want to
- write app-level periodic execution, or
- tighten wait precision.
When these two blend, the timer-selection discussion gradually drifts somewhere strange.
flowchart TB
accTitle: Separating periodic execution from wait precision
accDescr: Periodic work every few seconds as an app-level convenience and getting close to a deadline at millisecond scale are different problems, and the three timers are not dedicated tools for precision.
same["Want to do something at a fixed interval"] --> a["App-level periodic execution"]
same --> b["Tighten wait precision"]
a --> timers["Where the three timers come in"]
b --> design["Move on to designing how you wait"]
b -.-> note["The three timers are not precision tools"]
Figure 8: Even under the same phrase fixed interval, periodic execution and precision guarantees are separate problems.
4. Typical Patterns
4.1. For Async Periodic Work: PeriodicTimer
In a worker, a BackgroundService, or a resident console process,
if you want to run async work at a fixed interval, PeriodicTimer is the easiest to write with.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class CacheRefreshWorker : BackgroundService
{
private readonly ILogger<CacheRefreshWorker> _logger;
public CacheRefreshWorker(ILogger<CacheRefreshWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("CacheRefreshWorker started.");
await RefreshCacheAsync(stoppingToken);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await RefreshCacheAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CacheRefreshWorker stopping.");
}
}
private async Task RefreshCacheAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Refreshing cache...");
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
The virtues of this shape are that
- the code flow is easy to follow as a single
asyncmethod - the
CancellationTokenis easy to pass straight downstream - callback-based lifetime management and exception management shrink
It is an especially good fit when the body is I/O-wait-centric, such as
- calling HTTP
- querying a DB
- reading files
- awaiting other async APIs.
There are two cautions.
- Use it on a one-timer, one-consumer basis
- Decide for yourself what the policy is when the work takes longer than the period
PeriodicTimer does not automatically parallelize to catch up just because the previous iteration ran long.
In that sense, it is a timer for writing a fixed-interval async loop naturally.
If you also care about testability, the constructor overload that accepts a TimeProvider is quietly convenient as well.
flowchart TB
accTitle: The periodic loop with PeriodicTimer
accDescr: WaitForNextTickAsync waits for the next tick, the async body runs, and the loop waits again, all written as a single flow, and cancelling the CancellationToken exits the loop.
wait["Wait on WaitForNextTickAsync"] --> work["Run the async body"]
work --> wait
wait -.->|"Token cancelled"| exitl["Exit the loop and stop"]
work -.-> note["Delays are not parallelized automatically"]
Figure 9: PeriodicTimer lets you write wait, process, wait again as a single async method.
4.2. For Light Callbacks on the ThreadPool: System.Threading.Timer
If all you want is to invoke a short callback periodically, System.Threading.Timer is straightforward.
For example,
- emitting a heartbeat
- collecting light metrics
- inserting a short expiry check
- hanging off an existing callback-based design.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public sealed class HeartbeatService : IHostedService, IDisposable
{
private readonly ILogger<HeartbeatService> _logger;
private Timer? _timer;
private int _running;
public HeartbeatService(ILogger<HeartbeatService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void OnTimer(object? state)
{
if (Interlocked.Exchange(ref _running, 1) != 0)
{
return;
}
try
{
_logger.LogInformation("Heartbeat: {Now}", DateTimeOffset.Now);
}
finally
{
Volatile.Write(ref _running, 0);
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
The reason this example includes Interlocked.Exchange is that
System.Threading.Timer does not wait for the previous callback to complete.
This part is quite important.
- Callbacks run on the ThreadPool
- Callbacks are reentrant by assumption
- If the work runs longer than the interval, they can overlap
That they can overlap is something the sample unit tests make directly observable. Put 300 ms of work into a timer with a 50 ms period, count the maximum number of concurrent executions, and it reaches 2 or more. In the version with the same Interlocked.Exchange guard shown above, the maximum concurrency stays at 1 under the same conditions, and instead the callbacks that fire while the work is running are skipped.
// Excerpt from tests/KomuraSoft.TimerSelection.Tests/ThreadPoolTimerOverlapTests.cs
// No guard: 300 ms of work against a 50 ms period. Wait until overlap is observed
bool overlapped = await WaitUntilAsync(
() => Volatile.Read(ref maxObserved) >= 2,
TimeSpan.FromSeconds(10));
Assert.True(overlapped, "timer callbacks did not overlap within the timeout.");
// With the guard: under the same conditions the maximum concurrency stays at 1
Assert.Equal(1, Volatile.Read(ref maxConcurrent));
If the work is not light, it is less trouble to design it so that you
- skip duplicate invocations
- push onto a queue
- move to
PeriodicTimer.
flowchart TB
accTitle: Callback overlap and the guard
accDescr: System.Threading.Timer does not wait for the previous callback to complete, so callbacks can overlap when the work runs longer than the interval. An Interlocked.Exchange guard keeps concurrency at one and skips the callbacks that fire while the work is running.
fire["Callback fires every interval"] --> q{"Is the previous one still running?"}
q -->|"No guard"| overlap["Callbacks run overlapped"]
q -->|"With a guard"| skip["Skip this firing"]
skip --> one["Concurrency stays at one"]
Figure 10: With a timer that does not wait for the previous run, you decide yourself whether to allow overlap or reject it with a guard.
One more quietly important thing is to hold a reference.
A System.Threading.Timer becomes eligible for GC once no reference remains, even while it is running.
Also, even right after calling Dispose(), callbacks already queued may still run afterward.
So System.Threading.Timer is
- lightweight
- fast
- simple
but in exchange, it is a timer where you must properly shoulder the callback’s circumstances yourself.
flowchart TB
accTitle: Lifetime cautions for System.Threading.Timer
accDescr: A System.Threading.Timer becomes eligible for garbage collection once no reference remains, even while running, and callbacks already queued can still run right after Dispose is called.
t["System.Threading.Timer"] --> ref["Keep holding a reference"]
ref -.-> gc["Losing the reference makes it GC eligible"]
t --> disp["Clean up with Dispose"]
disp -.-> late["Queued callbacks can still run afterward"]
Figure 11: In exchange for being lightweight, holding the reference and handling callbacks after Dispose fall to you.
4.3. For WPF UI Updates: DispatcherTimer
If you want to periodically refresh an on-screen clock or a light status display in WPF, DispatcherTimer is natural.
using System;
using System.Windows;
using System.Windows.Threading;
public partial class MainWindow : Window
{
private readonly DispatcherTimer _clockTimer;
public MainWindow()
{
InitializeComponent();
_clockTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromSeconds(1)
};
_clockTimer.Tick += ClockTimer_Tick;
_clockTimer.Start();
}
private void ClockTimer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
protected override void OnClosed(EventArgs e)
{
_clockTimer.Stop();
_clockTimer.Tick -= ClockTimer_Tick;
base.OnClosed(e);
}
}
The DispatcherPriority.Background passed to the constructor specifies at which priority in the Dispatcher queue the Tick is processed. The parameterless new DispatcherTimer() also defaults to Background, so this only makes the default explicit; it does not change the behavior. Background (value 4) is the priority that runs after all other non-idle work has finished, sitting below Input (5) and Render (7). In other words, the Tick will not push input handling or rendering aside to run. That fits a clock display well, where a little drift is acceptable but getting in the way of the user is not. If you want the Tick result on screen as soon as possible, you can raise it to Normal (9), but then the assumption that the Tick body stays light becomes even stronger.
flowchart TB
accTitle: Where DispatcherPriority sits
accDescr: Background is the priority that runs after all other non-idle work has finished, below Input and Render, so the Tick does not push input or rendering aside. If the result must reach the screen sooner you can raise it to Normal.
tick["DispatcherTimer Tick"] --> bg["Queued at Background (value 4)"]
bg --> after["Processed after input and rendering"]
after -.-> fit["Fits a clock that must not get in the way"]
bg -.-> up["Raise to Normal (value 9) when it must be prompt"]
up -.-> cond["Which assumes an even lighter Tick"]
Figure 12: The default Background priority queues the Tick where it never pushes input or rendering aside.
The virtue of DispatcherTimer is that Tick is processed on WPF’s Dispatcher, so you can touch the UI directly.
This pairs well with, for example,
- clock displays
- light refresh of connection-status indicators
- triggers for re-evaluating Commands
- light updates of numbers shown on screen.
But here too, things change past a certain point.
DispatcherTimer runs on the UI thread, so
heavy work in the Tick handler drags down input, rendering, and layout along with it.
Also, DispatcherTimer is not a tool that guarantees firing exactly at the specified time.
It is affected by other work on the Dispatcher queue and by priorities.
So in practice, things stay stable if you keep in mind that you should
- keep the Tick body light
- move heavy I/O and CPU work elsewhere
- call
Stop()and unsubscribe on close, making the lifetime explicit.
flowchart TB
accTitle: Three things that keep DispatcherTimer stable
accDescr: Keep the Tick body light, move heavy I/O and CPU work elsewhere, and make the lifetime explicit with Stop and unsubscription when the window closes.
dt["Running a DispatcherTimer"] --> l1["Keep the Tick body light"]
dt --> l2["Move heavy work to the background"]
dt --> l3["Make the lifetime explicit with Stop and unsubscribe"]
l1 -.-> why["Because it spends UI thread time"]
Figure 13: In return for touching the UI directly, you need to keep the Tick light and make the lifetime explicit.
4.4. For Soft-Real-Time-Leaning Periodic Work: Look at Other Tools
This is the connection point to the previous article.
What the previous soft real-time article dealt with was not running roughly every so many seconds, but how to reduce period jitter and deadline misses.
In that context, the themes are
- not relying on
Sleep-based relative waits - using event-driven approaches and waitable timers
- splitting the fast path and the slow path
- measuring how late you are.
So it is cleanest to split the problem from the start, like this.
- Everyday async periodic work in an app
→
PeriodicTimer - ThreadPool callbacks
→
System.Threading.Timer - UI updates
→
DispatcherTimer - The accuracy of the period itself is the main concern → the world of the previous article
The question “I want to run as tightly as possible every 1 ms, so which .NET timer is best?” is, about halfway, no longer a timer-selection question but a question of how you wait and how you design it.
flowchart TB
accTitle: Split the problem into four from the start
accDescr: Everyday async periodic work in an app goes to PeriodicTimer, ThreadPool callbacks to System.Threading.Timer, UI updates to DispatcherTimer, and when the accuracy of the period itself is the main concern the question moves to designing how you wait, which is the soft real-time side.
p["What you want to do"] --> a["async: PeriodicTimer"]
p --> b["callback: Timer"]
p --> c["UI: DispatcherTimer"]
p --> d["Precision: designing how you wait"]
Figure 14: Before which timer, it is cleaner to split which problem this is into four.
5. Common Anti-Patterns
5.1. Passing an async Lambda Straight to System.Threading.Timer
This is a very common thing to do.
_timer = new Timer(async _ => await RefreshAsync(), null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
It looks tidy, but TimerCallback is void.
So this async lambda is effectively treated like async void.
As a result,
- the caller cannot await it
- completion cannot be waited on
- exception management becomes difficult
- callback overlap has to be considered separately
which is an awkward state to be in.
It is worth spelling out more clearly why exception management becomes difficult. With async Task, the exception rides on the Task, so the caller receives it at the point of await. An async void (equivalent) has no such Task, so the thrown exception is rethrown directly onto the SynchronizationContext that was current when the method started. A System.Threading.Timer callback runs on the ThreadPool, and there is no SynchronizationContext there. As a result, the exception becomes an unhandled exception on a ThreadPool thread and, by default, takes the whole process down. Unless you wrap the inside of the callback in your own try / catch, nothing on the outside can catch it.
flowchart TB
accTitle: Where an exception from an async lambda callback goes
accDescr: TimerCallback is void, so the async lambda passed to it becomes the equivalent of async void, and the exception is rethrown onto the SynchronizationContext that was current at start. Because there is none on the ThreadPool it becomes an unhandled exception that takes the process down by default.
ex["Exception inside the async lambda"] --> void["Escapes as the equivalent of async void"]
void --> ctx{"Is there a SynchronizationContext?"}
ctx -->|"None on the ThreadPool"| unh["Becomes a ThreadPool unhandled exception"]
unh --> crash["Takes the process down by default"]
ex -.-> guard["Only your own try / catch can stop it"]
Figure 15: The exception from that tidy-looking async lambda takes the process down with nowhere to be caught.
If the body is async, considering PeriodicTimer first reads better.
5.2. Putting Heavy Work in a DispatcherTimer Tick
DispatcherTimer can touch the UI directly, so the temptation is to write everything there.
But that is the UI thread.
Put in
- long synchronous work
- heavy CPU computation
- blocking I/O
- work containing long
awaits that can double-fire
and you collide head-on with UI input and rendering.
Keep the Tick body light, move heavy work to the background, and bring only the needed results back to the UI. That is more stable.
5.3. Assuming PeriodicTimer Automatically Makes Up for Delays
This is also an easy misunderstanding.
PeriodicTimer is excellent as a tool for cleanly writing fixed-interval async loops, but
it does not run iterations in parallel on its own to catch up when the previous one runs long.
The sample unit tests confirm this as well. Leave a timer with a 250 ms period alone for 1.5 seconds with nobody waiting on it, then wait: the first wait completes immediately from what has accumulated, but the second does not. The multiple ticks that occurred during that idle stretch have been coalesced into one.
// Excerpt from tests/KomuraSoft.TimerSelection.Tests/PeriodicTimerBehaviorTests.cs
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(250));
await Task.Delay(TimeSpan.FromMilliseconds(1500)); // nobody is waiting during this stretch
// The first wait completes immediately thanks to the accumulated tick
ValueTask<bool> first = timer.WaitForNextTickAsync();
Assert.True(first.IsCompleted);
Assert.True(await first);
// The second wait does not complete immediately (no multiple ticks are left over)
ValueTask<bool> second = timer.WaitForNextTickAsync();
Assert.False(second.IsCompleted);
Since ticks that occur while you are not waiting can be coalesced into one, you must decide by design whether
- to skip when late
- only the latest state matters
- every single iteration must be processed.
flowchart TB
accTitle: Coalescing of ticks that fire while nobody waits
accDescr: Multiple ticks that occur while nobody is awaiting a PeriodicTimer are coalesced into one, so whether to skip when late, look only at the latest state, or process every iteration has to be decided by design.
idle["Several ticks while nobody waits"] --> fold["Coalesced into one"]
fold --> q{"How to handle the delay?"}
q --> s1["Skip"]
q --> s2["Look at the latest only"]
q --> s3["Process every iteration"]
Figure 16: Accumulated ticks collapse into one. Catching up is not automatic; it is a design decision.
5.4. Postponing Stop and Lifetime Management
Timers go wrong more often when you stop them than when you start them.
The easy-to-miss items are these.
- Creating a
System.Threading.Timeras a local variable and not holding a reference - Leaving the
Dispose()story vague without stopping theSystem.Threading.Timer - Never calling
Stop()on aDispatcherTimerand never unsubscribing from Tick - The timer extending object lifetimes even after the screen has closed
DispatcherTimer in particular can keep alive the object its handler method is bound to.
If you get that odd feeling of this Window should be closed but it is still around, this is where to look.
flowchart TB
accTitle: Pitfalls in stopping and lifetime management
accDescr: A Timer whose reference is not held and a vague Dispose story leave stopping undefined, while a DispatcherTimer that is never stopped and never unsubscribed keeps the object it is bound to alive, showing up as a Window that should be closed but is still around.
m1["No reference held for the Timer"] --> trouble["Stopping stays undefined"]
m2["Vague Dispose story"] --> trouble
m3["Neither Stop nor Tick unsubscription"] --> keep["Keeps the bound object alive"]
keep --> ghost["A Window that should be closed is still around"]
Figure 17: Timers go wrong more often when you stop them than when you start them. Write the lifetime cleanup from the start.
6. Code Review Checklist
- Can you explain whether the periodic work should be written as a UI update / ThreadPool callback / async loop?
- Is an async body being forced into a callback-style timer?
- If using
System.Threading.Timer, can the code tolerate overlapping callbacks, or is it guarded? - Does the
DispatcherTimerTick contain heavy work, blocking I/O, or long synchronous processing? - If using
PeriodicTimer, is the policy for falling behind decided? - Are the stop method (
Change/Dispose/Stop) and the app-shutdown flow clear? - Is the reference to the
System.Threading.Timerproperly held? - Is there unsubscription and cleanup for the
DispatcherTimerwhen the screen closes? - Has the problem been split, at the outset, into app-level periodic execution versus wait precision?
7. A Rough Decision Guide
Here are practical rules of thumb.
-
Hit an API every 30 seconds to refresh settings →
PeriodicTimer -
Send a heartbeat or light metrics every 5 seconds →
System.Threading.Timer -
Show a clock or light status updates in WPF →
DispatcherTimer -
Touch the UI directly on every Tick →
DispatcherTimer -
The body of the periodic work is full of
awaits, and you want stopping and exceptions handled naturally →PeriodicTimer -
Add a small callback-based kick at low cost →
System.Threading.Timer -
Managing period precision and jitter at the 1-5 ms scale is the point → before these three, look at the wait methods in the previous article
Put very bluntly in one line each,
PeriodicTimeris the timer for asyncSystem.Threading.Timeris the timer for ThreadPool callbacksDispatcherTimeris the timer for the UI.
With this mnemonic, you rarely miss by much.
8. Summary
What really matters in choosing a .NET timer is not the difference in names, but these three points.
- Where it runs
- In what flow you want to write it
- How you handle overlap and delays
As policy, this alone is enough to hold your own.
- Async periodic work:
PeriodicTimer - Light callbacks on the ThreadPool:
System.Threading.Timer - WPF UI updates:
DispatcherTimer - When precision is the main concern, look at another way of waiting
Timers blur together because the names are similar. But their roles are not that similar.
PeriodicTimeris a tool for shaping the async flowSystem.Threading.Timeris a tool for periodically kicking callbacksDispatcherTimeris a tool for periodic updates on the UI thread
Just thinking of these three separately makes the code considerably quieter.
flowchart TB
accTitle: Summary of what the three timers are for
accDescr: Summarizes the difference in roles: PeriodicTimer shapes the async flow, System.Threading.Timer periodically kicks a callback, and DispatcherTimer updates periodically on the UI thread.
t["What the timers are for"] --> pt["PeriodicTimer: async"]
t --> st["Timer: callback"]
t --> dt["DispatcherTimer: UI"]
t -.-> rt["For precision, move to wait design"]
Figure 18: The names are similar but the roles are not. Remember them in these three categories and you will not miss by much.
Conversely, when they blend,
- what should be async ends up looking like
async void - the code crashes from touching the UI directly
- callbacks overlap and the state gets muddy
- the period-precision discussion gets lumped in too
and fairly ordinary kinds of trouble ensue.
Start by looking at where you want it to run. That alone makes choosing a timer a lot less fraught.
9. References
- Full sample code for this article (library, demo, unit tests) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/periodictimer-system-threading-timer-dispatchertimer-guide
- Related article: A Practical Guide to Getting as Close to Soft Real-Time as Possible on Ordinary Windows - The Checklist to Look at First
- Related article: A Practical Decision Table for C# async/await - Task.Run and ConfigureAwait
- Related article: WPF/WinForms async and the UI Thread on One Sheet
- Timers - .NET
- PeriodicTimer Class
- PeriodicTimer.WaitForNextTickAsync(CancellationToken) Method
- PeriodicTimer.Dispose Method
- PeriodicTimer Constructor
- Timer Class (System.Threading)
- Timer Constructor (System.Threading)
- Background tasks with hosted services in ASP.NET Core
- DispatcherTimer Class (System.Windows.Threading)
- DispatcherTimer Class (Microsoft.UI.Xaml)
- DispatcherTimer Constructor (Background is the default priority)
- DispatcherPriority Enum
- Timer Class (System.Windows.Forms) (accuracy is limited to about 55 milliseconds)
- Async/Await - Best Practices in Asynchronous Programming (how async void exceptions are handled)
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Why Use the .NET Generic Host and BackgroundService in Desktop Apps
How to use the Generic Host and BackgroundService to organize startup, periodic processing, shutdown, logging, configuration, and DI in W...
End of Servicing for Windows Printer Drivers — How Business Apps Should Prepare Their Report and Label Printing
Microsoft is phasing out v3/v4 printer drivers. What Windows protected print mode removes, and how to inventory and prepare report and la...
Practical Multithreading Best Practices: .NET Edition — What to Decide Before You Add More Threads
Keep .NET/C# threads from crashing or hanging. Ride on Task, cut shared mutable state, lock with discipline, stop with CancellationToken,...
Code Design for Business Systems — Deciding Product and Customer Codes, and Check Digits
A practical guide to deciding the code scheme for a business system, including product and customer codes. Covers a decision table for me...
CI/CD for WinForms / WPF Apps in Practice — Automating from Build to Signing and Distribution with GitHub Actions
A practical guide to setting up CI/CD for WinForms / WPF apps with GitHub Actions. Covers a minimal YAML for build+test on windows-latest...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
UI Threading & Timers
Topic page for WPF / WinForms UI threading, async flow, Dispatcher usage, and timer decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
In Windows application development involving periodic execution, UI updates, and background processing, timer selection directly affects implementation quality.
Technical Consulting & Design Review
If you are at the stage of sorting out where to split the responsibilities of PeriodicTimer and DispatcherTimer, this can be explored as a technical consulting and design review engagement.
Frequently Asked Questions
Common questions about the topic of this article.
- What is the difference between PeriodicTimer and System.Threading.Timer?
- The biggest difference is that PeriodicTimer is the style where you await each tick, while System.Threading.Timer is callback style. With PeriodicTimer you can write wait, process, wait again as a single async method, and a CancellationToken is easy to pass downstream. A System.Threading.Timer callback, by contrast, runs on the ThreadPool and does not wait for the previous callback to complete, so callbacks can overlap when the work runs longer than the interval. For asynchronous periodic work, PeriodicTimer suits better; for a periodic kick of a light synchronous callback, System.Threading.Timer does.
- Does PeriodicTimer automatically catch up when the work falls behind?
- No. It will not start running iterations in parallel to catch up just because the previous one ran long. If several ticks occur while you are not waiting, they are coalesced into one. So you have to decide by design whether to skip when late, whether looking at only the latest state is enough, or whether every single iteration must be processed. It is also not designed for multiple concurrent WaitForNextTickAsync calls on one timer.
- Is it wrong to pass an async lambda to System.Threading.Timer?
- Better to avoid it. TimerCallback is void, so an async lambda passed to it is effectively treated like async void. The caller cannot await it, completion cannot be waited on, exception management becomes difficult, and callback overlap has to be considered separately on top of that. If the body is async, considering PeriodicTimer first is both more readable and safer.
- When should DispatcherTimer be used?
- Use it in WPF when you want to refresh the UI periodically, for example a clock display or a light status indicator. Because Tick is processed on WPF's Dispatcher (the UI thread), the strength is that you can touch the UI directly inside the handler. But since it runs on the UI thread, heavy work or blocking I/O in Tick drags input and rendering down with it. It also does not guarantee firing exactly at the specified time. Things stay stable when you keep the Tick body light, move heavy work to the background, and make the lifetime explicit with Stop() and unsubscription when the window closes.