What it is
A cron-expression job scheduler for Go, supporting standard cron syntax, descriptive shortcuts, time zones and second-level precision.
Register functions against cron expressions and start the scheduler. Jobs run in their own goroutines; wrappers add recovery, skip-if-still-running and logging.
Installation
go get github.com/robfig/cron/v3Getting started
The smallest useful thing you can do with it, and what each part means.
go
c := cron.New(
cron.WithLocation(time.UTC), // never schedule in local time on a server
cron.WithChain(
cron.Recover(cron.DefaultLogger), // a panic must not kill the scheduler
cron.SkipIfStillRunning(cron.DefaultLogger),
),
)
c.AddFunc("0 3 * * *", func() { nightlyReport(ctx) }) // 03:00 daily
c.AddFunc("@every 5m", func() { refreshCache(ctx) })
c.Start()
defer c.Stop()Advanced usage
Where the library earns its place over a simpler alternative.
go
c.Start()
<-ctx.Done()
// Stop() prevents new runs and returns a context that closes when the
// jobs already running have finished.
stopCtx := c.Stop()
select {
case <-stopCtx.Done():
log.Println("all jobs finished")
case <-time.After(30 * time.Second):
log.Println("timed out waiting for jobs")
}Errors and fixes
The failures you are most likely to hit, and what actually resolves them.
- Jobs run at the wrong time
- The scheduler defaults to local time. Set WithLocation(time.UTC) and confirm the expression uses the standard five-field format.
- Every replica runs the job
- This scheduler is in-process and has no coordination. Guard the job with a database or Redis lock.
Best practices
- Always use WithLocation — relying on the server's local time zone causes daylight-saving surprises.
- Add Recover so one panicking job does not stop the whole scheduler.
- Use SkipIfStillRunning or DelayIfStillRunning for jobs that might overrun their interval.
- For multi-instance deployments, add a distributed lock — every replica runs its own scheduler.
Background
Why it exists, and what it was reacting to.
robfig/cron is the long-standing default for in-process scheduling in Go — the answer when you want periodic work without deploying a separate scheduler.
