A Golang job scheduling library that lets you run Go functions at pre-determined intervals using cron expressions, fixed durations, daily, weekly, monthly, or one-time schedules with support for distributed deployments.
WithIntervalFromCompletion and timing behaviors for duration-based jobs.
By default, gocron calculates intervals from the scheduled start time, not the completion time. This means jobs run at predictable times regardless of how long they take to execute.
WithIntervalFromCompletion() changes this behavior to calculate intervals from when the job completes.
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
doWork() // takes 2 minutes
}),
)
// Runs at: 09:00, 09:05, 09:10 (regardless of execution time)Timeline:
09:00 - Job starts (completes at 09:02)
09:05 - Job starts (completes at 09:07)
09:10 - Job starts (completes at 09:12)Next run is calculated from scheduled start, not actual completion.
Pros:
Cons:
Examples:
// Health check every 30 seconds (want regular timing)
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(checkHealth),
)
// Metrics collection every minute (want clock alignment)
j, _ = s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(collectMetrics),
)func WithIntervalFromCompletion() JobOptionCalculate next run from completion time:
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
doWork() // takes 2 minutes
}),
gocron.WithIntervalFromCompletion(),
)
// Runs at: 09:00, 09:07, 09:14 (5 minutes after each completion)Timeline:
09:00 - Job starts
09:02 - Job completes
09:07 - Job starts (5 min after 09:02, completes at 09:09)
09:14 - Job starts (5 min after 09:09)Pros:
Cons:
Examples:
// API scraping with rate limit
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(scrapeAPI),
gocron.WithIntervalFromCompletion(),
)
// Resource-intensive processing
j, _ = s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(processLargeDataset),
gocron.WithIntervalFromCompletion(),
)
// Variable-duration cleanup
j, _ = s.NewJob(
gocron.DurationJob(10*time.Minute),
gocron.NewTask(cleanupFiles), // Duration varies
gocron.WithIntervalFromCompletion(),
)WithIntervalFromCompletion() only affects duration-based schedules:
// APPLIES: DurationJob
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Works
)
// APPLIES: DurationRandomJob
j, _ = s.NewJob(
gocron.DurationRandomJob(time.Minute, 5*time.Minute),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Works
)Time-based schedules ignore this option:
// IGNORED: CronJob
j, _ := s.NewJob(
gocron.CronJob("*/5 * * * *", false),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Ignored
)
// IGNORED: DailyJob
j, _ = s.NewJob(
gocron.DailyJob(1, gocron.NewAtTimes(gocron.NewAtTime(9, 0, 0))),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Ignored
)
// IGNORED: WeeklyJob, MonthlyJob, OneTimeJob
// ... all time-based schedules ignore this optionTime-based schedules always run at their scheduled times.
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
time.Sleep(6 * time.Minute) // exceeds interval
}),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithIntervalFromCompletion(),
)Behavior:
Timeline:
09:00 - Start (completes 09:06)
09:11 - Start (5 min after 09:06, completes 09:17)
09:22 - Start (5 min after 09:17)j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
time.Sleep(6 * time.Minute)
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
gocron.WithIntervalFromCompletion(),
)Behavior:
WithIntervalFromCompletionTimeline:
09:00 - Run 1 starts (completes 09:06)
09:05 - Run 2 scheduled → queued
09:06 - Run 1 completes
09:11 - Run 2 starts (5 min after 09:06, completes 09:17)
09:10 - Run 3 scheduled → queued
09:15 - Run 4 scheduled → queued
09:17 - Run 2 completes
09:22 - Run 3 starts (5 min after 09:17)Queued runs execute with proper spacing.
Job takes 30 seconds, runs every minute:
Default (from start):
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
time.Sleep(30 * time.Second)
}),
)Timeline:
09:00:00 - Start (completes 09:00:30)
09:01:00 - Start (completes 09:01:30)
09:02:00 - Start (completes 09:02:30)30 seconds rest between runs.
With interval from completion:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
time.Sleep(30 * time.Second)
}),
gocron.WithIntervalFromCompletion(),
)Timeline:
09:00:00 - Start (completes 09:00:30)
09:01:30 - Start (completes 09:02:00)
09:03:00 - Start (completes 09:03:30)60 seconds rest between runs (guaranteed).
Job takes 2 minutes, runs every minute:
Default (from start):
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
time.Sleep(2 * time.Minute)
}),
)Timeline (without singleton mode):
09:00 - Run 1 starts (completes 09:02)
09:01 - Run 2 starts (completes 09:03) [OVERLAPPING]
09:02 - Run 3 starts (completes 09:04) [OVERLAPPING]Multiple instances running concurrently.
With interval from completion:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
time.Sleep(2 * time.Minute)
}),
gocron.WithIntervalFromCompletion(),
)Timeline:
09:00 - Start (completes 09:02)
09:03 - Start (completes 09:05)
09:06 - Start (completes 09:08)No overlap, 1 minute rest guaranteed.
Job takes 1-5 minutes randomly:
Default (from start):
j, _ := s.NewJob(
gocron.DurationJob(3*time.Minute),
gocron.NewTask(func() {
duration := time.Duration(rand.Intn(4)+1) * time.Minute
time.Sleep(duration)
}),
)Timeline:
09:00 - Start (takes 5 min, completes 09:05)
09:03 - Start (takes 2 min, completes 09:05) [OVERLAP at 09:03-09:05]
09:06 - Start (takes 1 min, completes 09:07)Unpredictable overlaps.
With interval from completion:
j, _ := s.NewJob(
gocron.DurationJob(3*time.Minute),
gocron.NewTask(func() {
duration := time.Duration(rand.Intn(4)+1) * time.Minute
time.Sleep(duration)
}),
gocron.WithIntervalFromCompletion(),
)Timeline:
09:00 - Start (takes 5 min, completes 09:05)
09:08 - Start (3 min after 09:05, takes 2 min, completes 09:10)
09:13 - Start (3 min after 09:10, takes 1 min, completes 09:14)Consistent 3-minute rest regardless of duration.
// Guarantee 1 minute between API calls
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
callExternalAPI() // Duration varies
}),
gocron.WithIntervalFromCompletion(),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Ensures compliance with API rate limits.
// Give system 5 minutes rest between processing
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
processLargeFiles() // Duration varies by file size
}),
gocron.WithIntervalFromCompletion(),
)Prevents system overload.
// Check for new items every 30 seconds after processing current batch
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
items := fetchPendingItems()
for _, item := range items {
process(item) // Duration depends on batch size
}
}),
gocron.WithIntervalFromCompletion(),
)Natural backpressure: larger batches cause longer intervals.
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(callRateLimitedAPI),
gocron.WithIntervalFromCompletion(),
)Guarantees minimum time between calls.
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(slowOperation),
gocron.WithIntervalFromCompletion(),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Prevents overlap and ensures rest time.
// Variable duration: use interval from completion
j, _ := s.NewJob(
gocron.DurationJob(2*time.Minute),
gocron.NewTask(variableDurationJob),
gocron.WithIntervalFromCompletion(),
)
// Consistent duration: use default (from start)
j, _ = s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(consistentDurationJob),
)// Metrics at regular intervals: use default
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(collectMetrics),
)
// Processing with rest: use from completion
j, _ = s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(processData),
gocron.WithIntervalFromCompletion(),
)Symptom: Jobs running at unpredictable times.
Cause: WithIntervalFromCompletion with variable duration.
Expected: This is normal behavior. Each run depends on previous completion.
Solution: If predictable timing is needed, remove WithIntervalFromCompletion.
Symptom: Jobs run more often than expected.
Cause: Job completes quickly, next run scheduled immediately + interval.
Example:
// Job takes 10 seconds, interval is 1 minute
// Runs every 70 seconds (10s + 60s)Solution: This is expected. The interval is the rest time, not total cycle time.
Symptom: Jobs don't align to wall clock (top of minute, etc.).
Cause: WithIntervalFromCompletion calculates from completion, not clock.
Solution: Remove WithIntervalFromCompletion for clock alignment:
// Want top of every hour
j, _ := s.NewJob(
gocron.DurationJob(time.Hour),
gocron.NewTask(myFunc),
// Don't use WithIntervalFromCompletion
)Symptom: WithIntervalFromCompletion seems ignored.
Cause: Using time-based schedule (not DurationJob).
Solution: Only works with DurationJob and DurationRandomJob:
// WRONG: CronJob ignores this
j, _ := s.NewJob(
gocron.CronJob("*/5 * * * *", false),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Ignored
)
// CORRECT: DurationJob respects this
j, _ = s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(myFunc),
gocron.WithIntervalFromCompletion(), // Works
)WithIntervalFromCompletionInstall with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2@2.19.1docs
api
examples
guides