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.Delayloops with cleaner, more reliable logic
It integrates perfectly with:
async/awaitBackgroundService- 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 reachedfalse→ 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
- Microsoft Docs — PeriodicTimer https://learn.microsoft.com/dotnet/api/system.threading.periodictimer
- Worker services and background tasks https://learn.microsoft.com/dotnet/core/extensions/workers
- Stephen Toub’s performance insights https://devblogs.microsoft.com/dotnet