CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-go-co-op-gocron-v2

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.

Overview
Eval results
Files

interval-calculation.mddocs/guides/concurrency/

Interval Calculation

WithIntervalFromCompletion and timing behaviors for duration-based jobs.

Overview

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.

Default Behavior: From Start Time

Basic Example

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.

Characteristics

Pros:

  • Predictable timing
  • Jobs run at regular clock intervals
  • Easy to reason about schedules
  • Aligns with wall clock

Cons:

  • Jobs can overlap if execution exceeds interval
  • No guaranteed rest time between executions
  • Variable-duration jobs may have unpredictable behavior

When to Use

  • Predictable timing is important
  • Jobs are idempotent
  • Job duration is well under interval
  • Clock alignment matters (e.g., top of every hour)

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),
)

WithIntervalFromCompletion

Basic Usage

func WithIntervalFromCompletion() JobOption

Calculate 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)

Characteristics

Pros:

  • Guaranteed rest time between executions
  • Jobs never overlap (implicit singleton behavior)
  • Good for rate limiting
  • Variable-duration jobs handled gracefully

Cons:

  • Unpredictable wall-clock timing
  • Schedule drift over time
  • Harder to reason about exact run times

When to Use

  • Rate limiting (guarantee downtime between runs)
  • Resource-intensive jobs (prevent overload)
  • Variable-duration jobs (maintain consistent pacing)
  • Jobs that shouldn't run back-to-back

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(),
)

Applicability

Applies To

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
)

Does Not Apply To

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 option

Time-based schedules always run at their scheduled times.

Combining with Singleton Mode

With LimitModeReschedule

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:

  • Job runs for 6 minutes
  • Next run scheduled for 5 minutes after completion
  • No overlapping executions
  • Predictable rest time

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)

With LimitModeWait

j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(func() {
        time.Sleep(6 * time.Minute)
    }),
    gocron.WithSingletonMode(gocron.LimitModeWait),
    gocron.WithIntervalFromCompletion(),
)

Behavior:

  • Job runs for 6 minutes
  • Scheduled runs queue up
  • Queued runs also respect WithIntervalFromCompletion
  • Each run starts 5 minutes after previous completion

Timeline:

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.

Comparison Examples

Example 1: Fast Jobs

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).

Example 2: Long Jobs

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.

Example 3: Variable Duration

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.

Advanced Patterns

Rate Limiting External API

// 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.

Resource-Intensive Processing

// 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.

Adaptive Polling

// 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.

Best Practices

1. Use for Rate Limiting

j, _ := s.NewJob(
    gocron.DurationJob(time.Minute),
    gocron.NewTask(callRateLimitedAPI),
    gocron.WithIntervalFromCompletion(),
)

Guarantees minimum time between calls.

2. Combine with Singleton Mode

j, _ := s.NewJob(
    gocron.DurationJob(5*time.Minute),
    gocron.NewTask(slowOperation),
    gocron.WithIntervalFromCompletion(),
    gocron.WithSingletonMode(gocron.LimitModeReschedule),
)

Prevents overlap and ensures rest time.

3. Match Interval to Job Characteristics

// 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),
)

4. Consider Monitoring Needs

// 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(),
)

Troubleshooting

Jobs Drifting from Schedule

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.

Jobs Running Too Frequently

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.

Jobs Not Running at Clock Times

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
)

Interval Not Applying

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
)

Related Documentation

  • API: Job OptionsWithIntervalFromCompletion
  • Singleton Mode — Prevent concurrent execution
  • Scheduler Limits — Global concurrency control
  • Creating Jobs — DurationJob, DurationRandomJob

Install with Tessl CLI

npx tessl i tessl/golang-github-com-go-co-op-gocron-v2

docs

index.md

tile.json