Choosing Between .NET's Three Timers - PeriodicTimer/Timer/DispatcherTimer

· Updated: · · C#, .NET, WPF, Timer, Design

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.

Three timers that share a name but not a characterShows 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.Three timersPeriodicTimer: wait with awaitTimer: callbacks arriveDispatcherTimer: 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 async lambda to System.Threading.Timer even 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 DispatcherTimer and 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.Timer is a single-threaded timer whose Tick is raised through the message loop, and it has no priority setting (DispatcherPriority) like DispatcherTimer does
  • 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.

What this article coversShows 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.How to write app-level periodic executionThe accuracy of the period itselfWhat is the subjectThe three timers in this articleThe 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

  1. The Conclusion First (In One Line)
  2. The One-Sheet Overview
    • 2.1. The Big Picture
    • 2.2. The First-Pass Decision Table
  3. 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
  4. 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
  5. Common Anti-Patterns
  6. Code Review Checklist
  7. A Rough Decision Guide
  8. Summary
  9. 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 await basis, start with PeriodicTimer
  • 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.Timer callbacks can overlap. Cramming async work in carelessly gets messy fast
  • DispatcherTimer lets 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.

  1. On which thread / context do you want it to run?
  2. Do you want to write the body sequentially with async / await?
  3. Can you tolerate overlapping callbacks?

Just separating these three makes things much less confusing.

The three questions to ask firstJust 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.Where should it runThe timer choice falls outWrite it sequentially with asyncCan overlapping callbacks be tolerated

Figure 3: Before the timer names, separate these three questions.

2. The One-Sheet Overview

2.1. The Big Picture

YesNoYesNoYesNoWant to do something at a fixed intervalRun it on the UI thread?DispatcherTimerWrite the bodyplainly withasync / await?PeriodicTimerRun light callbackson theThreadPool?System.Threading.TimerConsider another designChannel / 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.

What to look at when choosing a timerTimer 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.Choose by nameMiss where it runs and it goes wrongChoose by execution location and writing styleA 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.Timer and DispatcherTimer are callback / event style
  • PeriodicTimer is the style where you await the next tick

That is,

  • callback style means the timer calls you
  • PeriodicTimer means 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.

Callback style versus awaiting ticksShows 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.Callback styleAwait-the-tick styleWhich styleThe timer calls youYou wait for the next tickTimer and DispatcherTimerPeriodicTimerWait, 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
  • DispatcherTimer makes 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.

The trade-off behind where the timer runsSystem.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.Timer: runs on the ThreadPoolMarshal back explicitly to touch the UIDispatcherTimer: UI threadTouch the UI directlyHeavy 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.

Separating periodic execution from wait precisionPeriodic 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.Want to do something at a fixed intervalApp-level periodic executionTighten wait precisionWhere the three timers come inMove on to designing how you waitThe 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 async method
  • the CancellationToken is 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.

  1. Use it on a one-timer, one-consumer basis
  2. 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.

The periodic loop with PeriodicTimerWaitForNextTickAsync 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.Token cancelledWait on WaitForNextTickAsyncRun the async bodyExit the loop and stopDelays 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.
Callback overlap and the guardSystem.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.No guardWith a guardCallback fires every intervalIs the previous one still running?Callbacks run overlappedSkip this firingConcurrency 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.

Lifetime cautions for System.Threading.TimerA 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.System.Threading.TimerKeep holding a referenceLosing the reference makes it GC eligibleClean up with DisposeQueued 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.

Where DispatcherPriority sitsBackground 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.DispatcherTimer TickQueued at Background (value 4)Processed after input and renderingFits a clock that must not get in the wayRaise to Normal (value 9) when it must be promptWhich 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.
Three things that keep DispatcherTimer stableKeep 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.Running a DispatcherTimerKeep the Tick body lightMove heavy work to the backgroundMake the lifetime explicit with Stop and unsubscribeBecause 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.

Split the problem into four from the startEveryday 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.What you want to doasync: PeriodicTimercallback: TimerUI: DispatcherTimerPrecision: 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.

Where an exception from an async lambda callback goesTimerCallback 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.None on the ThreadPoolException inside the async lambdaEscapes as the equivalent of async voidIs there a SynchronizationContext?Becomes a ThreadPool unhandled exceptionTakes the process down by defaultOnly 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.
Coalescing of ticks that fire while nobody waitsMultiple 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.Several ticks while nobody waitsCoalesced into oneHow to handle the delay?SkipLook at the latest onlyProcess 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.Timer as a local variable and not holding a reference
  • Leaving the Dispose() story vague without stopping the System.Threading.Timer
  • Never calling Stop() on a DispatcherTimer and 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.

Pitfalls in stopping and lifetime managementA 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.No reference held for the TimerStopping stays undefinedVague Dispose storyNeither Stop nor Tick unsubscriptionKeeps the bound object aliveA 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 DispatcherTimer Tick 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.Timer properly held?
  • Is there unsubscription and cleanup for the DispatcherTimer when 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,

  • PeriodicTimer is the timer for async
  • System.Threading.Timer is the timer for ThreadPool callbacks
  • DispatcherTimer is 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.

  1. Where it runs
  2. In what flow you want to write it
  3. How you handle overlap and delays

As policy, this alone is enough to hold your own.

  1. Async periodic work: PeriodicTimer
  2. Light callbacks on the ThreadPool: System.Threading.Timer
  3. WPF UI updates: DispatcherTimer
  4. 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.

  • PeriodicTimer is a tool for shaping the async flow
  • System.Threading.Timer is a tool for periodically kicking callbacks
  • DispatcherTimer is a tool for periodic updates on the UI thread

Just thinking of these three separately makes the code considerably quieter.

Summary of what the three timers are forSummarizes the difference in roles: PeriodicTimer shapes the async flow, System.Threading.Timer periodically kicks a callback, and DispatcherTimer updates periodically on the UI thread.What the timers are forPeriodicTimer: asyncTimer: callbackDispatcherTimer: UIFor 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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog