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

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

In the previous article, A Practical Guide to Achieving Soft Real-Time as Much 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.

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 their characters differ considerably:

  • A timer whose ticks you await
  • A timer whose callbacks arrive on the ThreadPool
  • A timer that runs on the UI thread’s Dispatcher

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 with the same idea. For WinForms, it is more natural to look at System.Windows.Forms.Timer as the 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 soft real-time article.

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 verifying 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

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. Sloppily cramming async work into it 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 protagonists of high-precision waiting

In short, the first three things to look at are:

  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.

2. The One-Sheet Overview

2.1. The Big Picture

YesNoYesNoYesNoWant to do something at a fixed intervalWant it on the UI thread?DispatcherTimerWant to write the bodyplainly withasync / await?PeriodicTimerWant to fire lightcallbacks on theThreadPool?System.Threading.TimerConsider other designsChannel / BackgroundService / event / waitable timer

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 Writable on an await basis; stopping and cancellation are natural 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 Don’t make these three the protagonists - 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.

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: “the timer calls you”
  • PeriodicTimer: “you wait for the next tick”

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.”

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 “safe access to the UI,” but that simultaneously means “heavy work in it drags down input and rendering too.”

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 the writability of the async flow, 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.

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:

  • The code flow is easy to follow as a single async method
  • The CancellationToken is easy to pass straight downstream
  • Less callback-based lifetime management and exception management

It is an especially good fit when the body is I/O-wait-centric:

  • 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 yourself what the policy is when processing 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.

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, callbacks can overlap

If the work is not light, it is calmer to design with:

  • Skipping duplicate invocations
  • Pushing onto a queue
  • Moving to PeriodicTimer

One more quietly important thing: hold a reference. A System.Threading.Timer becomes eligible for GC once no reference remains, even while active. 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.

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 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, there is a point where the mood changes.

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 “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:

  • Keep the Tick body light
  • Offload heavy I/O and CPU work elsewhere
  • On close, call Stop() and unsubscribe, making 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 soft real-time article dealt with was not “running roughly every so many seconds is fine,” but how to reduce period jitter and deadline misses.

In that context, the themes are:

  • Don’t rely on Sleep-based relative waits
  • Use event-driven approaches and waitable timers
  • Split the fast path and the slow path
  • Measure the lateness

So it is cleanest to split the problem from the start:

  • Everyday async periodic work in an app → PeriodicTimer
  • ThreadPool callbacks → System.Threading.Timer
  • UI updates → DispatcherTimer
  • The accuracy of the period itself is the protagonist → the world of the previous article

The question “I want to run as tightly as possible every 1 ms — which .NET timer is best?” is, about halfway, no longer a timer-selection question but a question of wait methods and design.

5. Common Anti-Patterns

5.1. Passing an async Lambda Straight to System.Threading.Timer

This one is very tempting.

_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 must additionally be considered

— a fairly swampy state of affairs.

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, offload 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.

Since ticks occurring while you are not waiting can be coalesced into one, you must decide by design:

  • Do you skip when late?
  • Is only the latest state relevant?
  • Or must every single iteration be processed?

5.4. Postponing Stop and Lifetime Management

Timers cause more accidents in stopping than in running.

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, 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 is bound to. If you get that odd feeling of “huh, this Window should be closed but it’s still around,” this is where to look.

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. Precision as the protagonist: look at other wait methods

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.

Conversely, when they blend:

  • What should be async becomes async void-ish
  • The code crashes from touching the UI directly
  • Callbacks overlap and the state gets muddy
  • The period-precision discussion gets lumped in too

— fairly ordinary kinds of trouble ensue.

Start by looking at “where do you want it to run.” That alone makes timer selection a lot calmer.

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.

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