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.
Common issues and solutions for gocron.
Jobs are scheduled but never execute.
1. Scheduler Not Started
s, _ := gocron.NewScheduler()
j, _ := s.NewJob(...)
// Missing: s.Start()Solution: Call Start():
s.Start()2. Application Exits Before Execution
s, _ := gocron.NewScheduler()
j, _ := s.NewJob(...)
s.Start()
// Application exits immediatelySolution: Block main goroutine:
s.Start()
select {} // block foreverOr use proper shutdown:
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
s.Start()
<-ctx.Done()
s.Shutdown()3. Singleton Mode Blocking
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(2*time.Minute) // Takes longer than interval
}),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Solution: Increase interval or remove singleton mode:
// Option 1: Increase interval
gocron.DurationJob(3*time.Minute)
// Option 2: Use Wait mode
gocron.WithSingletonMode(gocron.LimitModeWait)Jobs run at unexpected times.
1. Timezone Issues
// Local time vs UTC confusion
j, _ := s.NewJob(
gocron.CronJob("0 9 * * *", false), // 9 AM in what timezone?
gocron.NewTask(dailyReport),
)Solution: Explicitly set timezone:
loc, _ := time.LoadLocation("America/New_York")
s, _ := gocron.NewScheduler(
gocron.WithLocation(loc),
)Or use cron timezone:
j, _ := s.NewJob(
gocron.CronJob("TZ=America/New_York 0 9 * * *", false),
gocron.NewTask(dailyReport),
)2. Cron Expression Errors
// Incorrect: runs every minute
j, _ := s.NewJob(
gocron.CronJob("* * * * *", false),
gocron.NewTask(hourlyTask),
)Solution: Verify cron expression:
// Correct: runs at minute 0 of every hour
j, _ := s.NewJob(
gocron.CronJob("0 * * * *", false),
gocron.NewTask(hourlyTask),
)3. Start Time Offset
// Job won't run until start time
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithStartAt(
gocron.WithStartDateTime(time.Now().Add(time.Hour)),
),
)Memory usage grows over time.
1. Queue Buildup with LimitModeWait
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(2*time.Minute) // Slow job
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
)Solution: Monitor and adjust:
// Monitor queue
go func() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
waiting := s.JobsWaitingInQueue()
if waiting > 100 {
log.Printf("WARNING: Queue growing: %d jobs", waiting)
}
}
}()
// Switch to Reschedule mode
gocron.WithSingletonMode(gocron.LimitModeReschedule)2. Goroutine Leaks in Job Functions
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
go func() {
// Goroutine never exits
for {
doWork()
}
}()
}),
)Solution: Use context for cleanup:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}()
}),
)Multiple instances of same job run simultaneously.
No concurrency control configured.
Use singleton mode:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Cause: Distributed locking not configured.
Solution: Use leader election or distributed locking:
// Option 1: Leader election
s, _ := gocron.NewScheduler(
gocron.WithDistributedElector(myElector),
)
// Option 2: Distributed locking
s, _ := gocron.NewScheduler(
gocron.WithDistributedLocker(myLocker),
)Cause: Clocks not synchronized.
Solution: Use NTP:
# Install and configure NTP
sudo apt-get install ntp
sudo systemctl enable ntp
sudo systemctl start ntpSymptom: Jobs take longer than expected.
Debug:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
start := time.Now()
defer func() {
log.Printf("Execution took: %v", time.Since(start))
}()
doWork()
}),
)Symptom: System overload.
Solution: Limit concurrency:
s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(10, gocron.LimitModeReschedule),
)// Bad: panic not recovered
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
panic("something went wrong")
}),
)Solution: Use error returns:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() error {
if err := doWork(); err != nil {
return fmt.Errorf("work failed: %w", err)
}
return nil
}),
gocron.WithEventListeners(
gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
log.Printf("Job failed: %v", err)
}),
),
)Cause: No shutdown timeout configured.
Solution: Set timeout:
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
defer s.Shutdown()Cause: Jobs don't check context.
Solution: Use context:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Println("Shutting down gracefully...")
cleanup()
return
default:
doWork()
}
}
}),
)type debugLogger struct{}
func (l *debugLogger) Debug(msg string, args ...any) {
log.Printf("[DEBUG] "+msg, args...)
}
// ... implement other methods
s, _ := gocron.NewScheduler(
gocron.WithLogger(&debugLogger{}),
)type debugMonitor struct{}
func (m *debugMonitor) JobScheduled(jobID uuid.UUID, job gocron.Job) {
log.Printf("Scheduled: %s next run: %v", job.Name(), job.NextRun())
}
func (m *debugMonitor) JobStarted(jobID uuid.UUID, job gocron.Job) {
log.Printf("Started: %s", job.Name())
}
func (m *debugMonitor) JobCompleted(jobID uuid.UUID, job gocron.Job, err error) {
log.Printf("Completed: %s error: %v", job.Name(), err)
}
// ... implement other methods
s, _ := gocron.NewScheduler(
gocron.WithSchedulerMonitor(&debugMonitor{}),
)jobs := s.Jobs()
for _, j := range jobs {
log.Printf("Job: %s", j.Name())
log.Printf(" Next run: %v", j.NextRun())
log.Printf(" Last run: %v", j.LastRun())
}Invalid cron expression.
Solution: Verify cron syntax:
// Bad: invalid expression
gocron.CronJob("* * * *", false) // Only 4 fields
// Good: valid expression
gocron.CronJob("* * * * *", false) // 5 fieldsJob cancelled during execution.
Cause: Scheduler shutdown or job removed.
Solution: Handle gracefully:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err() // Return context error
default:
return doWork()
}
}),
)Install with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2docs
api
examples
guides