Go Cron Jobs: robfig/cron, gocron & Native Scheduling Guide
Go's goroutines and channels make it uniquely suited for running scheduled tasks efficiently. Whether you use a simple time.Ticker in a goroutine, the battle-tested robfig/cron library, or the developer-friendly gocron package, Go gives you lightweight and reliable scheduling. This guide covers all three approaches with production-ready patterns.
Native Go Scheduling (time.Ticker, Goroutines)
For simple fixed-interval tasks, Go's standard library is all you need. A goroutine with a time.Ticker gives you a lightweight scheduler with zero dependencies:
time.Ticker: Fixed Interval
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
// Run immediately on startup, then every 30 seconds
doWork()
for range ticker.C {
doWork()
}
}
func doWork() {
fmt.Println(time.Now().Format(time.RFC3339), "- Running scheduled task")
// Your task logic here
}Goroutine with Context for Graceful Shutdown
In production, you need graceful shutdown. Use a context.Context to stop the ticker cleanly when your application receives a shutdown signal:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func startScheduler(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Println("Scheduler stopped gracefully")
return
case <-ticker.C:
// Run in a separate goroutine to not block the ticker
go func() {
if err := processQueue(); err != nil {
fmt.Printf("Task failed: %v\n", err)
}
}()
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Handle OS signals for graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go startScheduler(ctx)
<-sigCh
fmt.Println("Shutting down...")
cancel()
time.Sleep(2 * time.Second) // Allow in-flight tasks to complete
}When to use native scheduling
Native time.Ticker is perfect for simple fixed-interval tasks: health checks, polling, metrics collection. For anything that needs cron expression scheduling ("run at 2 AM on weekdays"), use a cron library instead.
robfig/cron v3: The Standard Library
robfig/cron is the most widely used cron library in the Go ecosystem with over 12,000 GitHub stars. It supports standard 5-field cron expressions, optional seconds precision, timezone handling, and job lifecycle management.
Installation
go get github.com/robfig/cron/v3Basic Usage
package main
import (
"fmt"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
// Every day at 2:00 AM
c.AddFunc("0 2 * * *", func() {
fmt.Println("Generating daily report...")
generateDailyReport()
})
// Every 15 minutes
c.AddFunc("*/15 * * * *", func() {
fmt.Println("Checking queue...")
processQueue()
})
// Every Monday at 9:00 AM
c.AddFunc("0 9 * * 1", func() {
fmt.Println("Sending weekly digest...")
sendWeeklyDigest()
})
c.Start()
// Block forever (or until shutdown signal)
select {}
}Seconds Precision
By default, robfig/cron v3 uses the standard 5-field format. To enable second-level precision, use the cron.WithSeconds() option:
// Enable 6-field cron expressions (with seconds)
c := cron.New(cron.WithSeconds())
// Every 30 seconds
c.AddFunc("*/30 * * * * *", func() {
fmt.Println("Running every 30 seconds")
})
// At second 0 of every minute (same as standard 5-field "* * * * *")
c.AddFunc("0 * * * * *", func() {
fmt.Println("Running every minute")
})Using the Job Interface
For more structured tasks, implement the cron.Job interface:
type CleanupJob struct {
DB *sql.DB
MaxAge time.Duration
}
func (j CleanupJob) Run() {
cutoff := time.Now().Add(-j.MaxAge)
result, err := j.DB.Exec("DELETE FROM sessions WHERE created_at < ?", cutoff)
if err != nil {
log.Printf("Cleanup failed: %v", err)
return
}
rows, _ := result.RowsAffected()
log.Printf("Cleaned up %d expired sessions", rows)
}
// Register the job
c.AddJob("0 3 * * *", CleanupJob{
DB: db,
MaxAge: 24 * time.Hour,
})Graceful Shutdown
func main() {
c := cron.New()
c.AddFunc("*/5 * * * *", processQueue)
c.Start()
// Wait for interrupt signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
// Stop accepting new jobs and wait for running ones to complete
ctx := c.Stop()
select {
case <-ctx.Done():
fmt.Println("All jobs completed, shutting down")
case <-time.After(30 * time.Second):
fmt.Println("Timeout waiting for jobs, forcing shutdown")
}
}gocron: Developer-Friendly Alternative
gocron provides a fluent, chainable API that some developers find more readable than raw cron expressions. It supports both cron expressions and human-readable schedules.
Installation
go get github.com/go-co-op/gocron/v2Fluent API
package main
import (
"fmt"
"time"
"github.com/go-co-op/gocron/v2"
)
func main() {
s, _ := gocron.NewScheduler()
// Using cron expression
s.NewJob(
gocron.CronJob("0 2 * * *", false),
gocron.NewTask(func() {
fmt.Println("Daily report at 2 AM")
}),
)
// Using duration-based scheduling
s.NewJob(
gocron.DurationJob(10 * time.Minute),
gocron.NewTask(func() {
fmt.Println("Every 10 minutes")
}),
)
// With a specific timezone
loc, _ := time.LoadLocation("America/New_York")
s.NewJob(
gocron.CronJob("0 9 * * MON-FRI", false),
gocron.NewTask(func() {
fmt.Println("Weekdays at 9 AM Eastern")
}),
gocron.WithLocation(loc),
)
s.Start()
// Block until signal
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
_ = s.Shutdown()
}robfig/cron vs gocron
| Feature | robfig/cron v3 | gocron v2 |
|---|---|---|
| API style | Cron-expression focused | Fluent / chainable |
| Cron expressions | 5 or 6 field | 5 or 6 field |
| Duration scheduling | Via @every syntax | Native DurationJob |
| Singleton mode | Via middleware/wrapper | Built-in option |
| Community | 12k+ stars, mature | 5k+ stars, active |
Timezone Handling in Go
Timezone misconfiguration is one of the most common causes of cron job timing issues. Go cron libraries default to the system's local timezone, which can produce unexpected results when deploying to cloud servers that typically run in UTC.
// robfig/cron: Set timezone at scheduler level
loc, err := time.LoadLocation("Europe/Berlin")
if err != nil {
log.Fatal(err)
}
c := cron.New(cron.WithLocation(loc))
// All jobs in this scheduler use Europe/Berlin timezone
c.AddFunc("0 9 * * *", morningTask) // 9:00 AM Berlin time
c.AddFunc("0 17 * * *", eveningTask) // 5:00 PM Berlin time
// Per-expression timezone override using CRON_TZ
c.AddFunc("CRON_TZ=America/New_York 0 9 * * *", func() {
// This runs at 9:00 AM New York time, regardless of scheduler timezone
})DST Warning
During Daylight Saving Time transitions, some times may not exist (spring forward) or occur twice (fall back). robfig/cron handles this by skipping non-existent times and running once for duplicate times. Always test your timezone-sensitive schedules around DST boundaries.
Error Handling Patterns
Panics in cron job functions can crash your entire application. Both robfig/cron and gocron provide mechanisms to handle errors gracefully.
Recover from Panics
// robfig/cron v3 has a built-in recovery wrapper
c := cron.New(
cron.WithChain(
cron.Recover(cron.DefaultLogger), // Recovers from panics and logs them
),
)
// Or wrap your function manually for custom error handling
func safeTask(fn func() error) func() {
return func() {
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC in cron job: %v\n%s", r, debug.Stack())
// Send alert to monitoring
}
}()
if err := fn(); err != nil {
log.Printf("Cron job error: %v", err)
// Send alert to monitoring
}
}
}
c.AddFunc("0 2 * * *", safeTask(func() error {
return generateReport()
}))Retry Pattern
func withRetry(maxRetries int, delay time.Duration, fn func() error) func() {
return func() {
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := fn(); err != nil {
log.Printf("Attempt %d/%d failed: %v", attempt, maxRetries, err)
if attempt < maxRetries {
time.Sleep(delay * time.Duration(attempt)) // Exponential backoff
continue
}
log.Printf("All %d attempts failed", maxRetries)
return
}
return // Success
}
}
}
c.AddFunc("*/10 * * * *", withRetry(3, 5*time.Second, syncExternalData))Monitoring with CronJobPro
Go services often run as long-lived processes where scheduled tasks execute inside the same binary. If the process crashes or a task starts failing silently, you need external monitoring to catch it. CronJobPro's dead man's switch pattern works perfectly here.
package main
import (
"fmt"
"net/http"
"time"
"github.com/robfig/cron/v3"
)
const pingURL = "https://cronjobpro.com/ping/abc123"
func monitoredTask(fn func() error) func() {
return func() {
// Signal start
http.Get(pingURL + "/start")
start := time.Now()
if err := fn(); err != nil {
// Signal failure
http.Post(pingURL+"/fail", "text/plain",
strings.NewReader(err.Error()))
return
}
// Signal success (CronJobPro tracks duration automatically)
http.Get(pingURL)
fmt.Printf("Task completed in %v\n", time.Since(start))
}
}
func main() {
c := cron.New()
c.AddFunc("0 2 * * *", monitoredTask(func() error {
return generateDailyReport()
}))
c.Start()
select {}
}CronJobPro alerts you when the expected ping does not arrive within the configured window, so you know immediately if your Go service went down or a task stopped running. Check the cron expression cheatsheet for common schedule patterns you can use with robfig/cron.
Frequently Asked Questions
What is the most popular cron library for Go?
robfig/cron v3 is the de facto standard with over 12,000 GitHub stars. It supports standard 5-field and optional 6-field (with seconds) cron expressions, handles timezones correctly, and is actively maintained. Most Go cron tutorials and production applications use this library.
How do I run a Go cron job with seconds precision?
By default, robfig/cron v3 uses 5-field standard cron expressions (minute granularity). To enable seconds precision, create the scheduler with the cron.WithSeconds() option: c := cron.New(cron.WithSeconds()). Then use 6-field expressions where the first field is seconds, like */30 * * * * * for every 30 seconds.
Should I use a cron library or native Go scheduling with goroutines?
Use native Go scheduling (time.Ticker or time.Sleep in a goroutine) for simple fixed-interval tasks like polling every 10 seconds. Use a cron library like robfig/cron when you need cron expression scheduling ("run at 2 AM", "run on weekdays only"), multiple jobs with different schedules, timezone handling, or job lifecycle management.
How do I handle timezone-aware cron jobs in Go?
robfig/cron v3 defaults to the machine's local timezone. To use a specific timezone, pass it when creating the scheduler: c := cron.New(cron.WithLocation(loc)). You can also use the CRON_TZ prefix in individual expressions. Always test timezone handling around DST transitions, as some times may be skipped or repeated.
How do I gracefully shut down a Go cron scheduler?
Both robfig/cron and gocron provide graceful shutdown mechanisms. For robfig/cron, call ctx := c.Stop() which returns a context that is done when all running jobs complete. Combine this with os.Signal handling: create a channel for SIGINT/SIGTERM, call c.Stop() when the signal arrives, and use a timeout to prevent hanging.
Monitor Your Go Cron Jobs
Get instant alerts when your Go scheduled tasks fail or miss a run. Free for up to 5 monitors.
Start Monitoring Free