A modern, high-performance job scheduling library for Go. JCRON is designed to be a flexible and efficient alternative to standard cron libraries, incorporating advanced scheduling features inspired by the Quartz Scheduler while maintaining a simple and developer-friendly API.
Core philosophy: Built on performance, readability, and robustness.
- Blazing fast performance - Core operations from 152ns to 289ns per calculation
- Sub-nanosecond bit operations - Critical path operations under 0.3ns
- Zero-allocation hot paths - Most operations with 0 allocations
- Advanced scheduling - L (last) and # (nth) patterns with optimal performance
- Memory-efficient - Smart caching with RWMutex protection
- Thread-safe - Concurrent-safe design from the ground up
- Features
- Installation
- Quick Start
- Core Concepts
- Cron Syntax
- PostgreSQL Integration
- Advanced Examples
- Performance
- API Reference
- Contributing
- ✅ Standard & Advanced Cron Syntax - Fully compatible with 5-field and 6-field Vixie-cron formats
- ✅ Enhanced Scheduling Rules - Quartz-like specifiers (L for last, # for nth occurrence)
- ✅ High-Performance Algorithm - Mathematical "next jump" calculation instead of tick-by-tick checking
- ✅ Aggressive Caching - Parse once, cache forever with integer-based representations
- ✅ Built-in Error Handling & Retries - Configurable retry policies with delays
- ✅ Panic Recovery - Jobs that panic won't crash the runner
- ✅ Structured Logging - Integration with standard log/slog library
- ✅ Thread-Safe - Safe for concurrent use across multiple goroutines
- ✅ PostgreSQL Integration - Database-backed job scheduling for distributed systems
- 🚀 Blazing fast operations - Core calculations 152-289ns per operation
- 💾 Memory efficient - Minimal allocations with smart caching
- 🔄 Smart caching - Schedule parsing happens only once
- ⚡ Fast-path optimizations - Special handling for common patterns
- 🎯 Zero-allocation operations for most schedule calculations
- ⚙️ Sub-nanosecond bit ops - 0.3ns for critical path operations
go get github.com/meftunca/jcronpackage main
import (
"fmt"
"log/slog"
"os"
"time"
"github.com/meftunca/jcron"
)
func main() {
// Create a structured logger
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Initialize the runner
runner := jcron.NewRunner(logger)
// Add a simple job - runs every 5 seconds
_, err := runner.AddFuncCron("*/5 * * * * *", func() error {
fmt.Println("Job executed at:", time.Now().Format(time.RFC3339))
return nil
})
if err != nil {
logger.Error("Failed to add job", "error", err)
return
}
// Start the runner
runner.Start()
defer runner.Stop()
logger.Info("JCRON runner started. Press CTRL+C to exit.")
// Keep running for demo
time.Sleep(30 * time.Second)
}package main
import (
"fmt"
"time"
"github.com/meftunca/jcron"
)
func main() {
// Create a new engine
engine := jcron.New()
// Define a schedule (weekdays at 9:30 AM)
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("30"),
Hour: jcron.StrPtr("9"),
DayOfMonth: jcron.StrPtr("*"),
Month: jcron.StrPtr("*"),
DayOfWeek: jcron.StrPtr("1-5"), // Monday to Friday
Year: jcron.StrPtr("*"),
Timezone: jcron.StrPtr("UTC"),
}
now := time.Now()
// Get next execution time
next, err := engine.Next(schedule, now)
if err != nil {
panic(err)
}
fmt.Printf("Next execution: %s\n", next.Format(time.RFC3339))
// Get previous execution time
prev, err := engine.Prev(schedule, now)
if err != nil {
panic(err)
}
fmt.Printf("Previous execution: %s\n", prev.Format(time.RFC3339))
}The Runner is the heart of the scheduler. It manages the entire lifecycle of all jobs:
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
runner := jcron.NewRunner(logger)
runner.Start()
defer runner.Stop()Jobs must implement the Job interface:
type Job interface {
Run() error
}For convenience, you can use function jobs:
runner.AddFuncCron("0 17 * * 5", func() error {
fmt.Println("TGIF! It's Friday 5 PM!")
return nil
})Configure automatic retries for failing jobs:
import "time"
_, err := runner.AddFuncCron(
"*/15 * * * * *",
myJob,
jcron.WithRetries(3, 5*time.Second), // 3 retries, 5s delay
)jcron supports both 6-field and 7-field cron expressions:
┌─────────── minute (0-59)
│ ┌───────── hour (0-23)
│ │ ┌─────── day of month (1-31)
│ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ ┌─── day of week (0-6 or SUN-SAT)
│ │ │ │ │ ┌─ year (optional)
│ │ │ │ │ │
* * * * * *
┌──────────── second (0-59)
│ ┌─────────── minute (0-59)
│ │ ┌───────── hour (0-23)
│ │ │ ┌─────── day of month (1-31)
│ │ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌─── day of week (0-6 or SUN-SAT)
│ │ │ │ │ │ ┌─ year (optional)
│ │ │ │ │ │ │
* * * * * * *
| Character | Description | Example | Meaning |
|---|---|---|---|
* |
Any value | * * * * * |
Every minute |
? |
Any value (alias for *) | 0 0 ? * MON |
Every Monday at midnight |
- |
Range | 0 9-17 * * * |
Every hour from 9 AM to 5 PM |
, |
List | 0 0 1,15 * * |
1st and 15th of every month |
/ |
Step | */5 * * * * |
Every 5 minutes |
L |
Last | 0 0 L * * |
Last day of every month |
# |
Nth occurrence | 0 0 * * MON#2 |
Second Monday of every month |
// Last day of month
"0 0 L * *"
// Last Friday of month
"0 22 * * 5L"
// Last weekday (Monday-Friday) of month
"0 18 * * 1-5L"// Second Tuesday of every month
"0 14 * * 2#2"
// First and third Monday of every month
"0 9 * * 1#1,1#3"
// Fourth Friday of every month
"0 17 * * 5#4"| Predefined | Equivalent | Description |
|---|---|---|
@yearly |
0 0 1 1 * |
Once a year at midnight on January 1st |
@annually |
0 0 1 1 * |
Same as @yearly |
@monthly |
0 0 1 * * |
Once a month at midnight on the 1st |
@weekly |
0 0 * * 0 |
Once a week at midnight on Sunday |
@daily |
0 0 * * * |
Once a day at midnight |
@midnight |
0 0 * * * |
Same as @daily |
@hourly |
0 * * * * |
Once an hour at the beginning of the hour |
JCRON includes a complete PostgreSQL implementation for database-backed job scheduling, perfect for distributed systems.
-- Load the JCRON PostgreSQL schema
\i sql-ports/psql.sqlimport (
"database/sql"
_ "github.com/lib/pq"
)
// Connect to PostgreSQL
db, err := sql.Open("postgres", "user=username dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Add a job
_, err = db.Exec(`
SELECT jcron.add_job_from_cron(
'daily_backup',
'@daily',
'pg_dump mydatabase > /backup/daily.sql',
'UTC'
)
`)
// Get next execution time
var nextRun time.Time
err = db.QueryRow(`
SELECT jcron.next_jump(
'{"s":"0","m":"30","h":"9","D":"*","M":"*","dow":"1-5"}'::jsonb,
NOW()
)
`).Scan(&nextRun)-- Job with retries and monitoring
SELECT jcron.add_job_from_cron(
'health_check',
'*/30 * * * * *', -- Every 30 seconds
'curl -f http://localhost:8080/health',
'UTC',
3, -- Max retries
300 -- Retry delay (seconds)
);
-- Get job statistics
SELECT * FROM jcron.get_job_stats('health_check');
-- Monitor failed jobs
SELECT job_name, error_message, failed_at
FROM jcron.failed_jobs_view;
-- Run maintenance
SELECT * FROM jcron.maintenance(30, true); -- Clean 30+ day logstype Schedule struct {
Second *string // 0-59 (optional, defaults to "*")
Minute *string // 0-59
Hour *string // 0-23
DayOfMonth *string // 1-31
Month *string // 1-12 or JAN-DEC
DayOfWeek *string // 0-6 or SUN-SAT (0=Sunday)
Year *string // Year (optional, defaults to "*")
Timezone *string // IANA timezone (optional, defaults to "UTC")
}
type Engine struct {
// Internal caching and optimization
}Creates a new cron engine with initialized cache.
engine := jcron.New()Calculates the next execution time after the given time.
schedule := jcron.Schedule{
Minute: jcron.StrPtr("30"),
Hour: jcron.StrPtr("14"),
// ... other fields
}
next, err := engine.Next(schedule, time.Now())Calculates the previous execution time before the given time.
prev, err := engine.Prev(schedule, time.Now())Utility function to create string pointers for schedule fields.
schedule := jcron.Schedule{
Minute: jcron.StrPtr("0"),
Hour: jcron.StrPtr("12"),
}jcron is optimized for production environments with enterprise-grade performance:
BenchmarkEngineNext_Simple-12 4065121 287.5 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_Complex-12 7477260 159.8 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_SpecialChars-12 468765 2563 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_Timezone-12 4754284 249.9 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_Frequent-12 7827499 152.4 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_Rare-12 429151 2816 ns/op 64 B/op 1 allocs/op
BenchmarkEnginePrev_Simple-12 6943513 174.2 ns/op 64 B/op 1 allocs/op
BenchmarkEnginePrev_Complex-12 7212504 167.9 ns/op 64 B/op 1 allocs/op
BenchmarkCacheHit-12 4162874 289.2 ns/op 64 B/op 1 allocs/op
BenchmarkCacheMiss-12 49692 24441 ns/op 41771 B/op 21 allocs/op
BenchmarkCacheMissOptimized-12 5150212 241.1 ns/op 64 B/op 1 allocs/op
BenchmarkExpandPart_Simple-12 48708877 24.53 ns/op 0 B/op 0 allocs/op
BenchmarkExpandPart_Range-12 10593228 115.1 ns/op 16 B/op 1 allocs/op
BenchmarkExpandPart_List-12 5934375 198.7 ns/op 96 B/op 1 allocs/op
BenchmarkExpandPart_Step-12 17353191 69.59 ns/op 16 B/op 1 allocs/op
BenchmarkFindNextSetBit-12 1000000000 0.2949 ns/op 0 B/op 0 allocs/op
BenchmarkFindPrevSetBit-12 1000000000 0.3011 ns/op 0 B/op 0 allocs/op
BenchmarkEngineNext_OptimizedSpecial-12 390615 3050 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_SimpleSpecial-12 375554 3220 ns/op 64 B/op 1 allocs/op
BenchmarkEngineNext_HashPattern-12 757720 1571 ns/op 64 B/op 1 allocs/op
BenchmarkSpecialCharsOptimized-12 24689727 48.71 ns/op 0 B/op 0 allocs/op
BenchmarkCacheKeyOptimized-12 21125938 57.28 ns/op 64 B/op 1 allocs/op
BenchmarkDirectAlgorithm_L_Pattern-12 678129 1774 ns/op 64 B/op 1 allocs/op
BenchmarkDirectAlgorithm_Hash_Pattern-12 767894 1583 ns/op 64 B/op 1 allocs/op
BenchmarkDirectAlgorithm_LastWeekday_Pattern-12 370545 3222 ns/op 64 B/op 1 allocs/op
BenchmarkStringOperations/Split-12 35390182 34.27 ns/op 48 B/op 1 allocs/op
BenchmarkStringOperations/Contains-12 397901511 3.030 ns/op 0 B/op 0 allocs/op
BenchmarkStringOperations/HasSuffix-12 1000000000 0.2935 ns/op 0 B/op 0 allocs/op
BenchmarkBitOperationsAdvanced/PopCount-12 1000000000 0.3004 ns/op 0 B/op 0 allocs/op
BenchmarkBitOperationsAdvanced/TrailingZeros-12 1000000000 0.2957 ns/op 0 B/op 0 allocs/op
- 🔥 Ultra-Fast (< 100 ns/op): Bit operations (0.29-0.30ns), simple parsing (24.53ns), optimized special chars (48.71ns), string ops (0.29-34.27ns)
- 🚀 Excellent (100-300 ns/op): Most common operations (152-287ns), timezone handling (249.9ns), cache hits (289.2ns), optimized cache miss (241.1ns)
- ✅ Good (300-3000 ns/op): Complex special character patterns (1571-3222ns), rare patterns (2816ns)
- 📈 Acceptable (> 3000 ns/op): Cold cache misses (24,441ns - happens only once per schedule)
- Zero-allocation bit operations - Core time calculations use CPU instructions (0.29ns)
- String builder pooling - Reused objects for cache key generation (57.28ns)
- Fast-path detection - Common patterns bypass complex parsing
- Pre-compiled pattern maps - Instant lookup for frequent special chars
- Efficient caching - RWMutex allows concurrent reads, optimized cache miss handling (241ns vs 24,441ns)
- Direct algorithm optimization - Special L/# patterns optimized to 1571-1774ns
// Monday to Friday, 9 AM to 5 PM, every 15 minutes
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("*/15"), // Every 15 minutes
Hour: jcron.StrPtr("9-17"), // 9 AM to 5 PM
DayOfMonth: jcron.StrPtr("*"), // Any day
Month: jcron.StrPtr("*"), // Any month
DayOfWeek: jcron.StrPtr("MON-FRI"), // Weekdays only
Year: jcron.StrPtr("*"), // Any year
Timezone: jcron.StrPtr("America/New_York"),
}// Last day of every month at 11:30 PM
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("30"),
Hour: jcron.StrPtr("23"),
DayOfMonth: jcron.StrPtr("L"), // Last day of month
Month: jcron.StrPtr("*"),
DayOfWeek: jcron.StrPtr("*"),
Timezone: jcron.StrPtr("UTC"),
}// First Monday of March, June, September, December at 2 PM
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("0"),
Hour: jcron.StrPtr("14"),
DayOfMonth: jcron.StrPtr("*"),
Month: jcron.StrPtr("3,6,9,12"), // Quarterly months
DayOfWeek: jcron.StrPtr("1#1"), // First Monday (#1)
Timezone: jcron.StrPtr("UTC"),
}// Every Saturday at 3 AM for maintenance
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("0"),
Hour: jcron.StrPtr("3"),
DayOfMonth: jcron.StrPtr("*"),
Month: jcron.StrPtr("*"),
DayOfWeek: jcron.StrPtr("SAT"), // Saturday only
Timezone: jcron.StrPtr("UTC"),
}// Every 5 seconds during business hours
schedule := jcron.Schedule{
Second: jcron.StrPtr("*/5"), // Every 5 seconds
Minute: jcron.StrPtr("*"),
Hour: jcron.StrPtr("9-17"),
DayOfMonth: jcron.StrPtr("*"),
Month: jcron.StrPtr("*"),
DayOfWeek: jcron.StrPtr("1-5"), // Weekdays
Timezone: jcron.StrPtr("UTC"),
}- Full support for ISO 8601 week-of-year (WOY) scheduling
- Use 7th field (WOY) in cron expressions or the
WeekOfYearfield in the API - All edge-cases (year boundaries, leap weeks, odd/even weeks, etc.) are covered by comprehensive tests
- Stable and API-compliant: all week-of-year logic matches ISO 8601 and passes 100% of edge-case tests
schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("0"),
Hour: jcron.StrPtr("12"),
DayOfWeek:jcron.StrPtr("1"), // Monday
WeekOfYear: jcron.StrPtr("1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53"),
}
next, _ := engine.Next(schedule, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(next)schedule := jcron.Schedule{
Second: jcron.StrPtr("0"),
Minute: jcron.StrPtr("0"),
Hour: jcron.StrPtr("8"),
DayOfWeek:jcron.StrPtr("1"), // Monday
Year: jcron.StrPtr("2025"),
WeekOfYear: jcron.StrPtr("10"),
}
next, _ := engine.Next(schedule, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC))
fmt.Println(next)- Odd/even weeks, first/last week, quarters, etc. (see Go and Node.js API)
- 100% of week-of-year edge-cases are covered by automated tests (see
week-of-year-cron.test.ts,week-of-year-test.tsin Node.js port) - All API and edge-cases are validated for both cron and JSON syntax
- Stable for production use
// ❌ Invalid
schedule.Timezone = jcron.StrPtr("PST")
// ✅ Correct
schedule.Timezone = jcron.StrPtr("America/Los_Angeles")// ❌ Invalid: 31st of February
schedule.DayOfMonth = jcron.StrPtr("31")
schedule.Month = jcron.StrPtr("2")
// ✅ Valid: Last day of February
schedule.DayOfMonth = jcron.StrPtr("L")
schedule.Month = jcron.StrPtr("2")// ⚠️ Uses OR logic (Vixie-style): 15th OR Monday
schedule.DayOfMonth = jcron.StrPtr("15")
schedule.DayOfWeek = jcron.StrPtr("MON")
// ✅ Specific: 15th of month only if it's Monday
schedule.DayOfMonth = jcron.StrPtr("15")
schedule.DayOfWeek = jcron.StrPtr("*")
// Then check if result.Weekday() == time.Monday- Reuse Engine instances - Engines maintain internal caches
- Use simple expressions when possible - Avoid special characters if not needed
- Pre-validate schedules - Check for errors during setup, not runtime
- Consider timezone impact - UTC is fastest for timezone-agnostic schedules
We welcome contributions! Please see our Contributing Guidelines for details.
# Clone the repository
git clone https://github.com/meftunca/jcron.git
cd jcron
# Run tests
go test -v ./...
# Run benchmarks
go test -bench=. -benchmem
# Run with race detection
go test -race -v ./...
# Test PostgreSQL integration (requires Docker)
docker-compose up -d postgres
go test -v ./sql-ports/...- Follow standard Go formatting (
go fmt) - Include comprehensive tests for new features
- Benchmark performance-critical changes
- Update documentation for API changes
JCRON is designed for high performance:
BenchmarkEngineNext_Simple 1000000 1.2 μs/op 0 allocs/op
BenchmarkEngineNext_Complex 500000 2.4 μs/op 0 allocs/op
BenchmarkEngineNext_SpecialChars 300000 4.1 μs/op 0 allocs/op
BenchmarkCacheHit 2000000 0.6 μs/op 0 allocs/op
The "next jump" algorithm provides sub-microsecond performance for most calculations.
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by the Quartz Scheduler for Java
- Thanks to the Go community for excellent tooling and libraries
- Special thanks to all contributors who have helped improve this project
🚀 Ready to schedule like a pro? Get started with JCRON today!
go get github.com/meftunca/jcron