Skip to content

PeriodicTimer in C# / .NET

Overview

PeriodicTimer is a lightweight, async-friendly timer introduced in .NET 6. It provides a simple way to run code at fixed intervals using await rather than callbacks.

It is designed for:

  • Background jobs
  • Scheduled internal processes
  • Polling loops
  • Health checks
  • Cache warmers
  • Replacing Task.Delay loops with cleaner, more reliable logic

It integrates perfectly with:

  • async/await
  • BackgroundService
  • Cancellation tokens
  • The .NET host

Why Use PeriodicTimer Instead of Task.Delay?

PeriodicTimer fixes known drawbacks of using a while loop with Task.Delay.

Problems with Task.Delay loops

  • Delay drift (each loop duration accumulates extra time)
  • Harder to cancel in the middle of a delay
  • Harder to synchronize between iterations
  • Awkward shutdown logic

PeriodicTimer advantages

Feature Task.Delay loop PeriodicTimer
Drift on long tasks Yes No (next tick is always based on period)
Cancellation support Good Better
Clean loop API
Simpler exception flow
More readable

Core API

public sealed class PeriodicTimer : IDisposable
{
    public PeriodicTimer(TimeSpan period);
    public ValueTask<bool> WaitForNextTickAsync(CancellationToken cancellationToken = default);
    public void Dispose();
}

Key Call

await timer.WaitForNextTickAsync(stoppingToken);

Returns:

  • true → next tick reached
  • false → timer disposed or period elapsed before completion

Basic Example

var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));

while (await timer.WaitForNextTickAsync())
{
    Console.WriteLine($"Tick at {DateTime.Now}");
}

Clean, readable, async, cancellation-aware.

Using PeriodicTimer in a BackgroundService (Typical Use Case)

public class CleanupWorker : BackgroundService
{
    private readonly ILogger<CleanupWorker> _logger;

    public CleanupWorker(ILogger<CleanupWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            _logger.LogInformation("Running periodic cleanup...");

            await PerformCleanupAsync(stoppingToken);
        }
    }

    private Task PerformCleanupAsync(CancellationToken ct)
        => Task.Delay(500, ct); // Example work
}

This is now the recommended pattern for recurring tasks inside workers.

Behaviour Explained in Detail

How WaitForNextTickAsync Works

sequenceDiagram
    participant Caller
    participant Timer

    Caller->>Timer: WaitForNextTickAsync()
    Timer-->>Caller: awaits period
    Caller->>Caller: Execute work
  • Each call to WaitForNextTickAsync() waits until the next interval.
  • It does not drift if the work inside the loop takes longer than expected.
  • If the timer is disposed, the method returns false.

6.2 Cancellation Behaviour

Cancellation during waiting → throws OperationCanceledException.

Error Handling Pattern

while (await timer.WaitForNextTickAsync(stoppingToken))
{
    try
    {
        await DoWorkAsync();
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Periodic task failed.");
    }
}

This is the recommended production pattern.

Avoiding Common Mistakes

Don’t forget to dispose the timer

var timer = new PeriodicTimer(...);
// WRONG: never disposed → potential memory leak

Use:

using var timer = new PeriodicTimer(...);

Don’t use PeriodicTimer for high-resolution timing

  • It is not real-time
  • Not accurate for sub-10 ms timing
  • Windows & Linux scheduling can vary

Don’t put infinite blocking code inside the loop

Don’t re-use a PeriodicTimer after it returns false

Once a timer completes/disposes → dead forever.

Alternatives and Comparison

PeriodicTimer vs Task.Delay loop

Aspect Task.Delay Loop PeriodicTimer
Accuracy Drifts Stable
Cancelling OK Best
Readability Meh Very clean
Exception handling Manual Straightforward
Recommended? No Yes

PeriodicTimer vs System.Timers.Timer

Feature PeriodicTimer System.Timers.Timer
async/await friendly ✘ callback-based
Perfect for BackgroundService ✘ (awkward)
Multi-threaded callbacks No Yes
Drift handling Better Worse
Ease of testing High Low

PeriodicTimer vs System.Threading.Timer

System.Threading.Timer executes on thread pool threads, not async-friendly.

Prefer PeriodicTimer unless you specifically need:

  • Multi-threaded invocation
  • Fire & forget behaviour

High-Level Architecture and Lifecycle

flowchart TD
    A["Initialization
new PeriodicTimer(period)"] --> B["Loop
WaitForNextTickAsync()"] B --> C["Do Work"] C --> B B -->|Cancellation or Dispose| D["Returns false"] D --> E["Exit Loop / Clean Up"]

Example: Periodic Timer with Dependency Injection

builder.Services.AddSingleton<PeriodicTimer>(_ =>
    new PeriodicTimer(TimeSpan.FromSeconds(10)));

Not common, but useful for coordinated polling.

Performance Considerations (.NET 10)

  • Very lightweight (no threads created)
  • Purely async-based
  • Uses efficient timer queue
  • More predictable under high load than classic timers
  • Ideal for high-throughput background job loops

Summary

PeriodicTimer is an async-first, lightweight timer introduced in .NET 6 that waits for the next interval via WaitForNextTickAsync. It provides drift-free interval scheduling, clean cancellation, and simple syntax for periodic background tasks.

PeriodicTimer is the recommended pattern for recurring work inside BackgroundService or long-running workers.

Compared to Task.Delay loops or legacy timers, PeriodicTimer is:

  • More accurate
  • More readable
  • More cancellation-friendly
  • Better suited for async workflows

Use it for periodic polling, cleanup jobs, scheduled maintenance, health checks, or any recurring asynchronous operation.

See Also

Further Reading