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.
WithSingletonMode, LimitModeReschedule vs LimitModeWait for preventing concurrent job execution.
Singleton mode prevents a job from running concurrently with itself. When a job is already running and its next scheduled time arrives, gocron uses the LimitMode to decide what to do:
func WithSingletonMode(mode LimitMode) JobOption
type LimitMode int
const (
LimitModeReschedule LimitMode = 1
LimitModeWait LimitMode = 2
)Prevents a job from running concurrently with itself:
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(myFunc),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Skip overlapping runs. Best for idempotent jobs where missing an execution is acceptable.
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute) // takes 60s, interval is 30s
}),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Runs at: 0s, 60s, 120s (skips runs at 30s, 90s)0s: Job starts
30s: Job still running → skip, reschedule for 60s
60s: Job completes, next run starts
90s: Job still running → skip, reschedule for 120s
120s: Job completes, next run startsIdeal for:
Example use cases:
// Health check every 30 seconds
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(checkHealth),
gocron.WithName("health-check"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Cache refresh every 5 minutes
j, _ = s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(refreshCache),
gocron.WithName("cache-refresh"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Metrics collection every minute
j, _ = s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(collectMetrics),
gocron.WithName("metrics"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Pros:
Cons:
Queue overlapping runs. All runs execute sequentially.
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute)
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Runs start at: 0s, then queue builds up0s: Job starts
30s: Job still running → queue run
60s: Job completes, queued run starts immediately
90s: Job still running → queue run
120s: Job completes, queued run starts immediatelyIdeal for:
Example use cases:
// Process queue items (all items matter)
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(processQueue),
gocron.WithName("queue-processor"),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Sequential database migrations
j, _ = s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(runMigrations),
gocron.WithName("migrations"),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Financial transactions (can't skip)
j, _ = s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(processTransactions),
gocron.WithName("transactions"),
gocron.WithSingletonMode(gocron.LimitModeWait),
)Pros:
Cons:
Monitor queue size to prevent unbounded growth:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(slowOperation),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Monitor queue size
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
waiting := s.JobsWaitingInQueue()
if waiting > 10 {
log.Printf("Warning: %d jobs waiting", waiting)
}
}
}()| Factor | LimitModeReschedule | LimitModeWait |
|---|---|---|
| Execution guarantee | May skip runs | All runs execute |
| Predictable timing | Yes (interval-based) | No (depends on queue) |
| Queue buildup | None | Possible if jobs are slow |
| Idempotent jobs | Ideal | Works |
| Sequential jobs | Not suitable | Ideal |
| Use when | Skipping is acceptable | Every run matters |
Is every execution critical?
├─ Yes → Use LimitModeWait
│ └─ Monitor queue size
│
└─ No → Use LimitModeReschedule
└─ Is the job idempotent?
├─ Yes → Perfect fit
└─ No → Consider redesigning jobBy default, jobs can run concurrently with themselves:
// No singleton mode
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute) // takes 60s, interval is 30s
}),
)
// Multiple instances run concurrently at: 0s, 30s, 60s, 90s...Timeline:
0s: Instance 1 starts
30s: Instance 2 starts (Instance 1 still running)
60s: Instance 1 completes, Instance 3 starts (Instance 2 still running)
90s: Instance 2 completes, Instance 4 starts (Instance 3 still running)When acceptable:
type SchedulerMonitor interface {
ConcurrencyLimitReached(limitType string, job Job)
// ... other methods
}Called when a job cannot start due to singleton mode:
type myMonitor struct{}
func (m *myMonitor) ConcurrencyLimitReached(limitType string, job gocron.Job) {
log.Printf("Singleton limit reached for job %s", job.Name())
// limitType: "singleton"
}
// ... implement other SchedulerMonitor methods
s, _ := gocron.NewScheduler(
gocron.WithSchedulerMonitor(&myMonitor{}),
)
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(slowJob),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)See Observability: Lifecycle Monitoring for details.
Apply singleton mode only when needed:
func createJob(s gocron.Scheduler, needsSingleton bool) {
options := []gocron.JobOption{
gocron.WithName("my-job"),
}
if needsSingleton {
options = append(options, gocron.WithSingletonMode(gocron.LimitModeReschedule))
}
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
options...,
)
}Ensure graceful cancellation:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
log.Println("Job cancelled, cleaning up...")
return
case <-ticker.C:
doWork()
}
}
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
)Use both modes for different jobs:
// Critical jobs: Wait mode
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(criticalOperation),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Non-critical jobs: Reschedule mode
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(statusCheck),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)// Idempotent → Reschedule
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(checkHealth),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Sequential → Wait
j, _ = s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(processBatch),
gocron.WithSingletonMode(gocron.LimitModeWait),
)go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
waiting := s.JobsWaitingInQueue()
if waiting > 50 {
log.Printf("Queue backlog: %d jobs", waiting)
}
}
}()// Good: interval > expected duration
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute), // Job takes ~2 minutes
gocron.NewTask(myFunc),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Bad: interval < expected duration (will always skip)
j, _ = s.NewJob(
gocron.DurationJob(30*time.Second), // Job takes 2 minutes
gocron.NewTask(slowFunc),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
select {
case <-ctx.Done():
return
default:
doWork()
}
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
)// Accessing database → Use singleton mode
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(updateDatabase),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)Symptom: Jobs scheduled but not executing.
Cause: Singleton mode blocking due to long-running execution.
Solution:
LimitModeReschedule instead of LimitModeWaitSymptom: JobsWaitingInQueue() keeps increasing.
Cause: Jobs take longer than interval with LimitModeWait.
Solution:
LimitModeRescheduleWithIntervalFromCompletion()Symptom: Jobs run at unexpected times.
Cause: LimitModeWait causing queued runs to execute immediately after completion.
Solution: Use LimitModeReschedule for predictable timing.
WithSingletonModeLimitModeInstall with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2@2.19.1docs
api
examples
guides