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.
Control how jobs run concurrently using singleton mode, scheduler limits, and interval strategies.
// If job is running when next run is scheduled, skip it
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute) // Takes longer than interval
}),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)// If job is running, queue the next run
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute)
}),
gocron.WithSingletonMode(gocron.LimitModeWait),
)// Reschedule mode: skip runs if previous is still executing
j1, _ := s.NewJob(
gocron.DurationJob(10*time.Second),
gocron.NewTask(func() {
fmt.Println("Start:", time.Now().Format("15:04:05"))
time.Sleep(25 * time.Second) // Longer than interval
fmt.Println("End:", time.Now().Format("15:04:05"))
}),
gocron.WithName("reschedule-mode"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Run 1: 0s-25s
// Run 2: 30s-55s (skipped 10s and 20s)
// Run 3: 60s-85s (skipped 40s and 50s)
// Wait mode: queue runs if previous is still executing
j2, _ := s.NewJob(
gocron.DurationJob(10*time.Second),
gocron.NewTask(func() {
fmt.Println("Start:", time.Now().Format("15:04:05"))
time.Sleep(25 * time.Second)
fmt.Println("End:", time.Now().Format("15:04:05"))
}),
gocron.WithName("wait-mode"),
gocron.WithSingletonMode(gocron.LimitModeWait),
)
// Run 1: 0s-25s
// Run 2: 25s-50s (queued at 10s)
// Run 3: 50s-75s (queued at 20s)// Ensure only one database cleanup runs at a time
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
fmt.Println("Starting database cleanup")
cleanupOldRecords()
vacuumTables()
updateStatistics()
fmt.Println("Database cleanup complete")
}),
gocron.WithName("db-cleanup"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)// Prevent multiple simultaneous API calls
j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
data, err := fetchFromExternalAPI()
if err != nil {
log.Printf("API call failed: %v", err)
return
}
processData(data)
}),
gocron.WithName("api-sync"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)// Allow max 3 jobs to run concurrently
s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(3, gocron.LimitModeReschedule),
)
// Add 10 jobs
for i := 0; i < 10; i++ {
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(id int) {
fmt.Printf("Job %d running\n", id)
time.Sleep(30 * time.Second)
}, i),
)
}
s.Start()
// At most 3 jobs run concurrentlys, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(3, gocron.LimitModeWait),
)
// Jobs wait in queue if limit reacheds, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(3, gocron.LimitModeWait),
)
// Add jobs...
s.Start()
// Later: check queue
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
waiting := s.JobsWaitingInQueue()
fmt.Printf("%d jobs waiting in queue\n", waiting)
}
}()// Limit concurrent jobs based on available resources
cpuCount := runtime.NumCPU()
maxJobs := cpuCount * 2 // 2x CPU cores
s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(maxJobs, gocron.LimitModeReschedule),
)
// Add resource-intensive jobs
for i := 0; i < 20; i++ {
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(id int) {
performHeavyComputation(id)
}, i),
gocron.WithName(fmt.Sprintf("computation-%d", i)),
)
}// Wait 5 minutes AFTER job completes, not from start
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(func() {
// Takes variable time to complete
doWork()
}),
gocron.WithIntervalFromCompletion(),
)// Process queue with consistent wait time between batches
j, _ := s.NewJob(
gocron.DurationJob(10*time.Second),
gocron.NewTask(func() {
batch := fetchQueueBatch()
processBatch(batch) // Variable duration
}),
gocron.WithIntervalFromCompletion(),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithName("queue-processor"),
)
// Always waits 10 seconds after batch completes// Ensure minimum time between API calls
j, _ := s.NewJob(
gocron.DurationJob(5*time.Second),
gocron.NewTask(func() {
// API call duration varies
data, err := callRateLimitedAPI()
if err != nil {
log.Printf("API error: %v", err)
return
}
processAPIData(data)
}),
gocron.WithIntervalFromCompletion(),
gocron.WithName("api-poller"),
)
// Waits 5 seconds after each call completes// Scheduler allows max 5 concurrent jobs
s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(5, gocron.LimitModeReschedule),
)
// Individual jobs use singleton mode
s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(doWork),
gocron.WithName("worker-1"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// This prevents both:
// 1. More than 5 jobs running across scheduler
// 2. Same job running multiple times concurrentlyj, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
// Variable duration work
processItems()
}),
gocron.WithIntervalFromCompletion(),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Best for ensuring consistent rest periods between runs
// and preventing overlaps// Apply singleton mode to all jobs
s, _ := gocron.NewScheduler(
gocron.WithGlobalJobOptions(
gocron.WithSingletonMode(gocron.LimitModeReschedule),
),
)
// All jobs inherit singleton mode
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
// Can override if needed
// gocron.WithSingletonMode(gocron.LimitModeWait),
)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) error {
// Create timeout context
timeoutCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
select {
case <-timeoutCtx.Done():
return fmt.Errorf("job timeout: %w", timeoutCtx.Err())
case <-time.After(doWork()):
return nil
}
}),
gocron.WithName("timeout-job"),
)ctx, cancel := context.WithCancel(context.Background())
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Job cancelled")
return
default:
if !processNextItem() {
return // No more items
}
}
}
}),
gocron.WithContext(ctx),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// Later: cancel to stop the job
cancel()package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-co-op/gocron/v2"
)
func main() {
s, _ := gocron.NewScheduler(
gocron.WithStopTimeout(30*time.Second),
)
defer s.Shutdown()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s.NewJob(
gocron.DurationJob(10*time.Second),
gocron.NewTask(func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Context cancelled, stopping work")
return
default:
fmt.Println("Working...")
time.Sleep(20 * time.Second)
fmt.Println("Work complete")
}
}),
gocron.WithContext(ctx),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
s.Start()
// Wait for interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
fmt.Println("Shutting down gracefully...")
cancel() // Cancel all job contexts
}j, _ := s.NewJob(
gocron.DurationJob(30*time.Second),
gocron.NewTask(func() {
time.Sleep(time.Minute)
}),
gocron.WithName("monitored-job"),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
gocron.WithEventListeners(
gocron.BeforeJobRuns(func(jobID uuid.UUID, jobName string) {
fmt.Printf("[%s] Starting\n", jobName)
}),
gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) {
fmt.Printf("[%s] Completed\n", jobName)
}),
),
)s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(3, gocron.LimitModeWait),
)
// Monitor queue
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
queueSize := s.JobsWaitingInQueue()
if queueSize > 10 {
log.Printf("WARNING: Queue buildup - %d jobs waiting", queueSize)
alertOnQueueBuildup(queueSize)
}
}
}()package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
"github.com/go-co-op/gocron/v2"
)
var activeJobs atomic.Int32
func main() {
// Create scheduler with global concurrency limit
s, err := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(5, gocron.LimitModeReschedule),
gocron.WithGlobalJobOptions(
gocron.WithSingletonMode(gocron.LimitModeReschedule),
),
)
if err != nil {
panic(err)
}
defer s.Shutdown()
// Fast job with singleton
s.NewJob(
gocron.DurationJob(5*time.Second),
gocron.NewTask(func() {
activeJobs.Add(1)
defer activeJobs.Add(-1)
fmt.Println("Fast job running")
time.Sleep(3 * time.Second)
}),
gocron.WithName("fast-job"),
)
// Slow job with interval from completion
s.NewJob(
gocron.DurationJob(10*time.Second),
gocron.NewTask(func() {
activeJobs.Add(1)
defer activeJobs.Add(-1)
fmt.Println("Slow job running")
time.Sleep(20 * time.Second)
fmt.Println("Slow job complete")
}),
gocron.WithName("slow-job"),
gocron.WithIntervalFromCompletion(),
)
// Cancellable job
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s.NewJob(
gocron.DurationJob(15*time.Second),
gocron.NewTask(func(ctx context.Context) {
activeJobs.Add(1)
defer activeJobs.Add(-1)
select {
case <-ctx.Done():
fmt.Println("Cancellable job stopped")
return
case <-time.After(10 * time.Second):
fmt.Println("Cancellable job complete")
}
}),
gocron.WithContext(ctx),
gocron.WithName("cancellable-job"),
)
// Start scheduler
s.Start()
fmt.Println("Scheduler started")
// Monitor active jobs and queue
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
active := activeJobs.Load()
waiting := s.JobsWaitingInQueue()
fmt.Printf("Status: %d active, %d waiting\n", active, waiting)
}
}()
// Wait for interrupt
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
<-sigCh
fmt.Println("Shutting down...")
cancel() // Cancel context-aware jobs
}Reschedule for time-sensitive jobs (skip if too late)Wait for jobs that must run (queue them)WithSingletonMode, WithIntervalFromCompletionWithLimitConcurrentJobs, WithGlobalJobOptionsInstall with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2@2.19.1docs
api
examples
guides