Recurring Tasks Best Practices
Best practices for task keys, schedule choice, long-running tasks, time zones, and limits.
1. Always Use Task Keys for Recurring Tasks
// ✅ Good: Prevents duplicates on restart
await dispatcher.Dispatch(
new DailyTask(),
r => r.Schedule().EveryDay(),
taskKey: "daily-task");
// ❌ Bad: Creates duplicate every restart
await dispatcher.Dispatch(
new DailyTask(),
r => r.Schedule().EveryDay());
2. Use Meaningful Task Keys
// ✅ Good: Descriptive and unique
taskKey: "cleanup-expired-sessions"
taskKey: "user-{userId}:daily-summary"
taskKey: "tenant-{tenantId}:billing:monthly"
// ❌ Bad: Generic or unclear
taskKey: "task1"
taskKey: "job"
3. Choose the Right Schedule Format
Use the fluent API for readability when the pattern is straightforward. Save cron for complex schedules that would be awkward to express with the fluent API.
// ✅ Good: Fluent API is clear and readable for simple schedules
builder => builder.Schedule().EveryDay().AtTime(new TimeOnly(14, 0))
// ✅ Good: Cron shines for complex patterns
builder => builder.Schedule().UseCron("*/15 9-17 * * 1-5")
// ❌ Avoid: Cron makes simple patterns harder to understand
builder => builder.Schedule().UseCron("0 14 * * *")
4. Handle Long-Running Recurring Tasks
If your task takes a while to complete, set a timeout that gives it room to breathe. Also respect cancellation tokens so tasks can be stopped gracefully.
public class LongRunningRecurringHandler : EverTaskHandler<LongRunningRecurringTask>
{
public override TimeSpan? Timeout => TimeSpan.FromMinutes(30); // Generous timeout for long operations
public override async Task Handle(LongRunningRecurringTask task, CancellationToken cancellationToken)
{
// Check cancellation periodically so the task can be stopped cleanly
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessBatchAsync(i, cancellationToken);
}
}
}
5. Name the Time Zone Instead of Converting to UTC
A schedule with no zone is computed in UTC, which is the right answer when you mean an absolute cadence. When you mean a local hour, say so with InTimeZone and let EverTask resolve the offset at each occurrence:
// ✅ Good: an absolute daily cadence, stated as such
await dispatcher.Dispatch(
new GlobalTask(),
r => r.Schedule().EveryDay().AtTime(new TimeOnly(0, 0)), // Midnight UTC
taskKey: "global-midnight-task");
// ✅ Good: 9 AM in the user's zone, all year round
await dispatcher.Dispatch(
new UserTask(user.Id),
r => r.Schedule().EveryDay().AtTime(new TimeOnly(9, 0)).InTimeZone(user.TimeZoneId),
taskKey: $"user-{user.Id}-daily-task");
// ❌ Wrong: converting once freezes the offset, and the task drifts by an hour at the next DST change
var userTimeZone = TimeZoneInfo.FindSystemTimeZoneById(user.TimeZoneId);
var localTime = TimeZoneInfo.ConvertTimeToUtc(DateTime.Today.AddHours(9), userTimeZone);
await dispatcher.Dispatch(
new UserTask(user.Id),
r => r.Schedule().EveryDay().AtTime(TimeOnly.FromDateTime(localTime)),
taskKey: $"user-{user.Id}-daily-task");
InTimeZone also settles what happens on the two days a year a local hour is missing or repeated. See Time Zones.
6. Limit Recurring Tasks Appropriately
Use MaxRuns() and RunUntil() for tasks that shouldn’t run forever:
// ✅ Good: Time-limited trial features
await dispatcher.Dispatch(
new TrialFeatureTask(userId),
r => r.Schedule().EveryDay().RunUntil(trialEndDate),
taskKey: $"trial-{userId}");
// ✅ Good: Fixed-duration campaigns
await dispatcher.Dispatch(
new PromotionalEmailTask(),
r => r.Schedule().EveryWeek().OnDay(DayOfWeek.Friday).MaxRuns(4),
taskKey: "monthly-promo");
7. Monitor Recurring Task Health
It’s worth having a watchdog task that checks if your other recurring tasks are behaving:
await dispatcher.Dispatch(
new MonitorRecurringTasksTask(),
r => r.Schedule().EveryHour(),
taskKey: "monitor-recurring-tasks");
public class MonitorRecurringTasksHandler : EverTaskHandler<MonitorRecurringTasksTask>
{
private readonly ITaskStorage _storage;
private readonly IAlertService _alertService;
public override async Task Handle(MonitorRecurringTasksTask task, CancellationToken cancellationToken)
{
var recurringTasks = await _storage.Get(t => t.IsRecurring, cancellationToken);
foreach (var t in recurringTasks)
{
if (t.Status == QueuedTaskStatus.Failed && t.CurrentRunCount > 0)
{
await _alertService.SendAlertAsync(
$"Recurring task '{t.TaskKey}' is failing",
cancellationToken);
}
}
}
}
Next Steps
- Overview - Introduction to recurring tasks
- Fluent Scheduling API - Build type-safe schedules
- Managing Recurring Tasks - Cancel and monitor tasks
- Task Orchestration - Continuations and workflow patterns
- Scalability - Multi-queue and sharded scheduler
- Resilience - Retry policies and error handling