A maintained fork of robfig/cron — the most popular cron library for Go — with critical bug fixes, DST handling improvements, and modern toolchain support.
The original robfig/cron has been unmaintained since 2020, accumulating 50+ open PRs and several critical panic bugs that affect production systems. Rather than waiting indefinitely, this fork provides:
| Issue | Original | This Fork |
|---|---|---|
| TZ= parsing panics | Crashes on malformed input | Fixed (#554, #555) |
| Chain decorators | Entry.Run() bypasses chains |
Properly invokes wrappers (#551) |
| DST spring-forward | Jobs silently skipped | Runs immediately (ISC behavior, #541) |
| DOM/DOW logic | OR (confusing) | AND (logical, consistent) |
| Go version | Stuck on 1.13 | Go 1.25+ with modern toolchain |
go get github.com/netresearch/go-cronimport cron "github.com/netresearch/go-cron"Note
Requires Go 1.25 or later.
Drop-in replacement — just change the import path:
// Before
import "github.com/robfig/cron/v3"
// After
import cron "github.com/netresearch/go-cron"The API is 100% compatible with robfig/cron v3. However, this fork includes knowingly accepted behavior changes that fix bugs and inconsistencies in the unmaintained upstream — see the comparison table above for a summary.
Warning
Behavior differences exist. While the API is compatible, some runtime behavior has changed (DOM/DOW matching, DST handling, chain execution). Review docs/MIGRATION.md before upgrading production systems.
package main
import (
"fmt"
"time"
cron "github.com/netresearch/go-cron"
)
func main() {
c := cron.New()
// Run every minute
c.AddFunc("* * * * *", func() {
fmt.Println("Every minute:", time.Now())
})
// Run at specific times
c.AddFunc("30 3-6,20-23 * * *", func() {
fmt.Println("In the range 3-6am, 8-11pm")
})
// With timezone
c.AddFunc("CRON_TZ=Asia/Tokyo 30 04 * * *", func() {
fmt.Println("4:30 AM Tokyo time")
})
c.Start()
// Keep running...
select {}
}Standard 5-field cron format (minute-first):
| Field | Required | Values | Special Characters |
|---|---|---|---|
| Minutes | Yes | 0-59 | * / , - |
| Hours | Yes | 0-23 | * / , - |
| Day of month | Yes | 1-31 | * / , - ? |
| Month | Yes | 1-12 or JAN-DEC | * / , - |
| Day of week | Yes | 0-6 or SUN-SAT | * / , - ? |
| Entry | Description | Equivalent |
|---|---|---|
@yearly |
Once a year, midnight, Jan 1 | 0 0 1 1 * |
@monthly |
Once a month, midnight, first day | 0 0 1 * * |
@weekly |
Once a week, midnight Sunday | 0 0 * * 0 |
@daily |
Once a day, midnight | 0 0 * * * |
@hourly |
Once an hour, beginning of hour | 0 * * * * |
@every <duration> |
Every interval | e.g., @every 1h30m |
For cyclic fields, ranges where start > end wrap around the boundary:
// Run from 10pm to 2am (spans midnight)
c.AddFunc("0 22-2 * * *", nightJob)
// Run Friday through Monday (spans weekend)
c.AddFunc("0 9 * * FRI-MON", weekendJob)
// Run November through February (spans year boundary)
c.AddFunc("0 0 1 NOV-FEB *", winterJob)Supported fields: seconds, minutes, hours, day-of-month, day-of-week, month. Non-existent days (e.g., Feb 31) are simply skipped.
Enable Quartz-compatible seconds field:
// Seconds field required
cron.New(cron.WithSeconds())
// Seconds field optional
cron.New(cron.WithParser(cron.NewParser(
cron.SecondOptional | cron.Minute | cron.Hour |
cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)))When both day-of-month and day-of-week are specified, both must match (AND logic). This is consistent with all other cron fields and enables useful patterns:
// Last Friday of month (days 25-31 AND Friday)
c.AddFunc("0 0 25-31 * FRI", lastFridayJob)
// First Monday of month (days 1-7 AND Monday)
c.AddFunc("0 0 1-7 * MON", firstMondayJob)
// Friday the 13th
c.AddFunc("0 0 13 * FRI", unluckyJob)Note
This differs from robfig/cron which uses OR logic. For migration compatibility,
use the DowOrDom option or see docs/MIGRATION.md.
Specify timezone per-schedule using CRON_TZ= prefix:
// Runs at 6am New York time
c.AddFunc("CRON_TZ=America/New_York 0 6 * * *", myFunc)
// Legacy TZ= prefix also supported
c.AddFunc("TZ=Europe/Berlin 0 9 * * *", myFunc)Or set default timezone for all jobs:
nyc, _ := time.LoadLocation("America/New_York")
c := cron.New(cron.WithLocation(nyc))This library implements ISC cron-compatible DST behavior:
| Transition | Behavior |
|---|---|
| Spring Forward (hour skipped) | Jobs in skipped hour run immediately after transition |
| Fall Back (hour repeats) | Jobs run once, during first occurrence |
| Midnight DST (midnight doesn't exist) | Automatically normalized to valid time |
Tip
For DST-sensitive applications, schedule jobs outside typical transition hours (1-3 AM) or use UTC.
See docs/DST_HANDLING.md for comprehensive DST documentation including examples, testing strategies, and edge cases.
Add cross-cutting behavior using chains:
// Apply to all jobs
c := cron.New(cron.WithChain(
cron.Recover(logger), // Recover panics
cron.SkipIfStillRunning(logger), // Skip if previous still running
))
// Apply to specific job
job := cron.NewChain(
cron.DelayIfStillRunning(logger), // Queue if previous still running
).Then(myJob)Available wrappers:
- Recover — Catch panics, log, and continue
- SkipIfStillRunning — Skip execution if previous run hasn't finished
- DelayIfStillRunning — Queue execution until previous run finishes
Compatible with go-logr/logr:
// Verbose logging
c := cron.New(cron.WithLogger(
cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)),
))Handle jobs that were missed while the scheduler was not running (e.g., application restart):
// Load last run time from your database
lastRun := loadFromDatabase("daily-report")
c.AddFunc("0 9 * * *", dailyReport,
cron.WithPrev(lastRun), // When it last ran
cron.WithMissedPolicy(cron.MissedRunOnce), // Run once if missed
cron.WithMissedGracePeriod(2*time.Hour), // Only if within 2 hours
)Policies:
MissedSkip(default) — No catch-up, wait for next scheduled timeMissedRunOnce— Run once immediately for the most recent missed executionMissedRunAll— Run for every missed execution (capped at 100 for safety)
Important
The scheduler does NOT persist state. You must provide the last run time via WithPrev()
and store it yourself (database, file, etc.). See docs/PERSISTENCE_GUIDE.md
for complete integration patterns.
Full documentation: pkg.go.dev/github.com/netresearch/go-cron
Contributions are welcome! Please read CONTRIBUTING.md before submitting PRs.
For security issues, please see SECURITY.md.
MIT License — see LICENSE for details.
This fork is maintained by Netresearch. The original cron library was created by Rob Figueiredo.