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.
How to update, remove, query job status, and trigger jobs manually with RunNow.
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithName("my-job"),
gocron.WithTags("worker", "cleanup"),
)
// Get job metadata
id := j.ID() // uuid.UUID
name := j.Name() // string
tags := j.Tags() // []stringfunc (j Job) LastRun() (time.Time, error)Returns the time of the last execution:
lastRun, err := j.LastRun()
if errors.Is(err, gocron.ErrJobNotFound) {
log.Println("Job was removed")
} else if err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Printf("Last run: %v\n", lastRun)
}Returns ErrJobNotFound if the job was removed.
func (j Job) NextRun() (time.Time, error)
func (j Job) NextRuns(count uint) ([]time.Time, error)Query upcoming execution times:
// Next scheduled run
nextRun, err := j.NextRun()
if err == nil {
fmt.Printf("Next run: %v\n", nextRun)
}
// Next 5 scheduled runs
nextFive, err := j.NextRuns(5)
if err == nil {
for i, t := range nextFive {
fmt.Printf("Run %d: %v\n", i+1, t)
}
}func (s Scheduler) Jobs() []JobReturns all registered jobs, sorted by ID:
allJobs := s.Jobs()
for _, j := range allJobs {
fmt.Printf("Job: %s (%s)\n", j.Name(), j.ID())
nextRun, _ := j.NextRun()
fmt.Printf(" Next run: %v\n", nextRun)
fmt.Printf(" Tags: %v\n", j.Tags())
}func (j Job) RunNow() errorTriggers job immediately without affecting schedule:
j, _ := s.NewJob(
gocron.DailyJob(1, gocron.NewAtTimes(gocron.NewAtTime(9, 0, 0))),
gocron.NewTask(doReport),
gocron.WithName("daily-report"),
)
// Normal schedule: runs at 9 AM
// Manual trigger: run now (doesn't affect 9 AM schedule)
err := j.RunNow()
if errors.Is(err, gocron.ErrJobRunNowFailed) {
log.Println("Failed to trigger job")
}Behavior:
ErrJobRunNowFailed if scheduler is not reachableUse cases:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(func() {
time.Sleep(2 * time.Minute) // Long-running
}),
gocron.WithSingletonMode(gocron.LimitModeReschedule),
)
// If job is already running, RunNow respects singleton mode
err := j.RunNow()
// May skip if already running (LimitModeReschedule)func (s Scheduler) Update(
id uuid.UUID,
jobDefinition JobDefinition,
task Task,
jobOptions ...JobOption,
) (Job, error)Replaces job definition, task, and options:
j, _ := s.NewJob(
gocron.DurationJob(5*time.Minute),
gocron.NewTask(myFunc),
gocron.WithName("my-job"),
)
// Later: change interval to 10 minutes
j, err := s.Update(
j.ID(),
gocron.DurationJob(10*time.Minute), // new interval
gocron.NewTask(myFunc), // same or new task
gocron.WithName("my-job"), // re-specify all options
)
if err != nil {
log.Printf("Update failed: %v", err)
}Behavior:
// Change task function
j, _ = s.Update(
j.ID(),
gocron.DurationJob(5*time.Minute), // keep same schedule
gocron.NewTask(myNewFunc), // new task
gocron.WithName("my-job"),
)// Change schedule, keep task
j, _ = s.Update(
j.ID(),
gocron.DurationJob(10*time.Minute), // new schedule
gocron.NewTask(myFunc), // keep same task
gocron.WithName("my-job"),
)// Add singleton mode
j, _ = s.Update(
j.ID(),
gocron.DurationJob(5*time.Minute),
gocron.NewTask(myFunc),
gocron.WithName("my-job"),
gocron.WithSingletonMode(gocron.LimitModeReschedule), // new option
)Returns ErrJobNotFound if the job doesn't exist:
_, err := s.Update(
uuid.New(), // non-existent ID
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
)
if errors.Is(err, gocron.ErrJobNotFound) {
log.Println("Job not found")
}func (s Scheduler) RemoveJob(id uuid.UUID) errorRemoves a job by ID:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
)
// Later: remove the job
err := s.RemoveJob(j.ID())
if errors.Is(err, gocron.ErrJobNotFound) {
log.Println("Job already removed")
}Behavior:
ErrJobNotFound if job doesn't existfunc (s Scheduler) RemoveByTags(tags ...string)Removes all jobs with at least one of the specified tags:
// Add jobs with tags
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(task1),
gocron.WithTags("cleanup", "database"),
)
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(task2),
gocron.WithTags("cleanup", "files"),
)
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(task3),
gocron.WithTags("reporting"),
)
// Remove all cleanup jobs (removes task1 and task2)
s.RemoveByTags("cleanup")
// Remove jobs with "temporary" OR "test" tag
s.RemoveByTags("temporary", "test")Tag matching: A job is removed if it has ANY of the specified tags (OR logic).
Jobs are automatically removed when:
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithLimitedRuns(3), // Removed after 3 runs
)end := time.Now().Add(24 * time.Hour)
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithStopAt(gocron.WithStopDateTime(end)), // Removed after 24h
)j, _ := s.NewJob(
gocron.OneTimeJob(gocron.OneTimeJobStartImmediately()),
gocron.NewTask(myFunc), // Removed after single execution
)func (s Scheduler) JobsWaitingInQueue() intReturns the number of jobs queued due to concurrency limits:
s, _ := gocron.NewScheduler(
gocron.WithLimitConcurrentJobs(3, 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)
}
}
}()See Concurrency: Scheduler Limits for details.
Track jobs for later management:
type JobRegistry struct {
jobs map[string]gocron.Job
mu sync.RWMutex
}
func (r *JobRegistry) Register(name string, job gocron.Job) {
r.mu.Lock()
defer r.mu.Unlock()
r.jobs[name] = job
}
func (r *JobRegistry) Get(name string) (gocron.Job, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
job, ok := r.jobs[name]
return job, ok
}
func (r *JobRegistry) TriggerByName(name string) error {
job, ok := r.Get(name)
if !ok {
return fmt.Errorf("job %s not found", name)
}
return job.RunNow()
}
// Usage
registry := &JobRegistry{jobs: make(map[string]gocron.Job)}
j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithName("my-job"),
)
registry.Register("my-job", j)
// Later: trigger by name
registry.TriggerByName("my-job")// Update job based on config changes
func updateJobFromConfig(s gocron.Scheduler, jobID uuid.UUID, config Config) error {
var interval time.Duration
switch config.Frequency {
case "fast":
interval = time.Minute
case "normal":
interval = 5 * time.Minute
case "slow":
interval = 15 * time.Minute
default:
return fmt.Errorf("invalid frequency: %s", config.Frequency)
}
_, err := s.Update(
jobID,
gocron.DurationJob(interval),
gocron.NewTask(myFunc),
gocron.WithName(config.Name),
)
return err
}// Remove jobs based on criteria
func removeInactiveJobs(s gocron.Scheduler) {
for _, j := range s.Jobs() {
lastRun, err := j.LastRun()
if err != nil {
continue
}
// Remove if not run in 24 hours
if time.Since(lastRun) > 24*time.Hour {
log.Printf("Removing inactive job: %s", j.Name())
s.RemoveJob(j.ID())
}
}
}func printJobStatus(s gocron.Scheduler) {
jobs := s.Jobs()
fmt.Printf("Total jobs: %d\n", len(jobs))
fmt.Printf("Jobs waiting in queue: %d\n", s.JobsWaitingInQueue())
fmt.Println("\nJob Details:")
for _, j := range jobs {
fmt.Printf(" %s (ID: %s)\n", j.Name(), j.ID())
fmt.Printf(" Tags: %v\n", j.Tags())
lastRun, err := j.LastRun()
if err == nil {
fmt.Printf(" Last run: %v (%v ago)\n", lastRun, time.Since(lastRun))
}
nextRun, err := j.NextRun()
if err == nil {
fmt.Printf(" Next run: %v (in %v)\n", nextRun, time.Until(nextRun))
}
}
}// Good: store ID for later updates
jobID := j.ID()
saveJobID(jobID) // persist if needed
// Later: update using stored ID
s.Update(jobID, newDefinition, newTask)j, _ := s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(myFunc),
gocron.WithName("my-job"), // Easy to identify
)// Tag temporary jobs
s.NewJob(
gocron.DurationJob(time.Minute),
gocron.NewTask(task1),
gocron.WithTags("temporary", "experiment"),
)
// Later: remove all temporary jobs
s.RemoveByTags("temporary")nextRun, err := j.NextRun()
if errors.Is(err, gocron.ErrJobNotFound) {
log.Println("Job was removed")
} else if err != nil {
log.Printf("Error: %v", err)
} else {
fmt.Printf("Next run: %v\n", nextRun)
}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)
}
}
}()Symptom: ErrJobNotFound when querying or updating job.
Causes:
WithLimitedRunsSolution: Check if job still exists before operations:
jobs := s.Jobs()
found := false
for _, job := range jobs {
if job.ID() == myJobID {
found = true
break
}
}
if !found {
log.Println("Job no longer exists")
}Symptom: RunNow() returns error or doesn't execute.
Causes:
Solution: Check scheduler state and concurrency:
if err := j.RunNow(); err != nil {
log.Printf("RunNow failed: %v", err)
log.Printf("Jobs waiting: %d", s.JobsWaitingInQueue())
}Symptom: Job continues with old schedule/task.
Cause: Running execution not interrupted.
Solution: Wait for current execution to complete:
s.Update(j.ID(), newDefinition, newTask)
// Next run will use new definitionUpdate, RemoveJob, RemoveByTags, JobsRunNow, LastRun, NextRun, NextRunsInstall with Tessl CLI
npx tessl i tessl/golang-github-com-go-co-op-gocron-v2docs
api
examples
guides