Skip to content

Scheduling

Use WithCron to run a job on a schedule:

app.AddJob("DailyReport", async () => { /* ... */ })
.WithCron("0 9 * * *", "America/Chicago"); // 9am Central every day

The second argument is an optional timezone ID. Without it, the schedule runs in UTC. Schedules respect the zone’s daylight saving transitions, so 0 9 * * * with America/Chicago always fires at 9:00 AM Central even when the equivalent UTC time shifts.

Each scheduled fire time produces at most one run, even with multiple nodes scheduling in parallel.

Some common expressions:

ExpressionSchedule
* * * * *Every minute
0 * * * *Every hour
0 9 * * *Daily at 9am
0 9 * * 1-5Weekdays at 9am
0 0 1 * *First of every month at midnight

If no node is available when a cron job is supposed to fire, the missed occurrences are called misfires. The misfire policy controls what happens when scheduling resumes:

app.AddJob("Cleanup", async () => { /* ... */ })
.WithCron("0 * * * *")
.WithMisfirePolicy(MisfirePolicy.FireOnce);
PolicyBehaviorWhen to use
Skip (default)Ignore missed fires and resume from the next future occurrence.Most jobs, where catch-up runs would pile up after an outage.
FireOnceFire once to catch up, then resume the normal schedule.Cumulative work that should run at least once after a gap.
FireAllFire every missed occurrence, optionally capped with fireAllLimit.Workloads where every scheduled execution matters and skipping any would cause data loss.

To bound the catch-up after long outages, pass fireAllLimit. Surefire keeps the most recent N misses and skips the rest, so a 24-hour outage on an hourly job with fireAllLimit: 10 fires the last 10 hours and resumes from now:

app.AddJob("Cleanup", async () => { /* ... */ })
.WithCron("0 * * * *")
.WithMisfirePolicy(MisfirePolicy.FireAll, fireAllLimit: 50);

A continuous job restarts after each run, regardless of whether it succeeded, failed, or was canceled. Useful for queue consumers, stream processors, and background pollers that should always be running.

app.AddJob("WatchFeed", async (CancellationToken ct) =>
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(ct))
{
// process feed
}
})
.Continuous();

Continuous jobs default to MaxConcurrency of 1, meaning only one instance runs across the cluster. Override it to run parallel workers:

app.AddJob("WatchFeed", async (CancellationToken ct) => { /* ... */ })
.Continuous()
.WithMaxConcurrency(3);

Surefire seeds enabled continuous jobs up to their configured MaxConcurrency on startup.

ScenarioBehavior
Job completesRestarts
Job fails, retries remainingNormal retry behavior
Job fails, retries exhaustedRestarts after a cooldown delay
Job canceledRestarts (unless job is disabled)
Job disabled via dashboardNo restart
Job re-enabled via dashboardRestarts