croner
Croner is a zero-dependency JavaScript and TypeScript cron parser and in-process scheduler. It runs callbacks from cron expressions or one-time dates, calculates past and future occurrences, supports named time zones, and understands extensions such as seconds, years, last days, nearest weekdays, and nth weekdays. It works in Node 18+, Bun, Deno, and browsers, with controls for pausing, stopping, overlap protection, error handling, and timer unref. It does not persist jobs or coordinate them across processes.
Croner is one of the better in-memory cron libraries: compact, carefully documented, timezone-aware, and unusually good at expression inspection. Do not use it as a substitute for a durable distributed scheduler.
Use it if
- You need a small in-process scheduler with timezone and daylight-saving handling
- You need to parse cron expressions or show upcoming run times without actually scheduling a task
- You want overlap protection for an async callback without adding a queue system
- Your app targets several JavaScript runtimes and you want both ESM and CommonJS entry points
- The job must survive restarts or run exactly once across several replicas: Croner stores schedules only in memory, so restarts lose state and every process will create its own timer
- You are scheduling business-critical work that needs retries, durable history, locks, backpressure, or an operator dashboard: use a job queue or platform scheduler instead
- You plan to run meaningful background work in a browser: closed tabs, throttled timers, suspended devices, and sleeping browsers make execution unreliable
- You assume every cron implementation agrees on day-of-month and day-of-week: Croner defaults to OR, adds its own + operator for AND, and has an optional Quartz weekday-number mode
Setup reality
Installation is genuinely small: one dependency-free package, included types, ESM and CommonJS builds, and Node 18 or newer. Production setup is harder than the import suggests. Choose an explicit timezone, add a catch handler, decide whether long tasks may overlap, stop jobs during shutdown, and ensure only one replica owns each schedule.
Patterns
Run every five secondsschedule-interval
import { Cron } from 'croner'
const job = new Cron('*/5 * * * * *', () => {
console.log('five seconds elapsed')
})Six fields include seconds. A conventional five-field pattern starts at minutes, so count fields before copying a schedule.
Run at a named local timeschedule-timezone
const reportJob = new Cron('0 30 8 * * MON-FRI', {
timezone: 'America/New_York',
}, () => {
sendDailyReport()
})Named IANA timezones account for daylight-saving changes; a fixed utcOffset does not.
Prevent overlapping async runsprotect-async-job
const syncJob = new Cron('0 * * * * *', {
protect: true,
catch: error => console.error('sync failed', error),
}, async () => {
await syncRemoteData()
})protect skips a trigger while the previous callback is busy; it does not queue a catch-up run or retry failures.
Calculate future occurrencesinspect-next-runs
const schedule = new Cron('0 0 9 * * MON', { paused: true })
const dates = schedule.nextRuns(4)
console.log(dates.map(date => date.toISOString()))Use paused when the object is only a parser; otherwise providing a callback later can turn it into an active timer.
Test whether a date matchesmatch-date
const mondays = new Cron('0 0 0 * * MON', { paused: true })
console.log(mondays.match('2026-08-10T00:00:00'))Without timezone or utcOffset, matching follows the runtime's local timezone.
Schedule one run from an ISO local timeschedule-once
new Cron('2026-12-24T09:00:00', {
timezone: 'Europe/Stockholm',
catch: console.error,
}, () => {
sendHolidayReminder()
})With a timezone option, the offset-free ISO string is interpreted as local time in that zone and fires once.
Pause, resume, trigger, and stopcontrol-job
job.pause()
job.resume()
await job.trigger()
job.stop()stop is permanent and cannot be followed by resume; create a new Cron instance if the schedule is needed again.
Stop automatically after a fixed countlimit-runs
const warmup = new Cron('*/10 * * * * *', {
maxRuns: 3,
catch: console.error,
}, () => refreshCache())maxRuns counts triggers for this in-memory instance; it is not durable across process restarts.
Require day-of-month and weekdayrequire-both-day-fields
const firstMonday = new Cron('0 12 1 * +MON', {
timezone: 'UTC',
}, () => runMonthlyTask())Croner normally treats day-of-month and day-of-week as OR. The + prefix makes this pattern require both.
Let a Node process exit with a scheduled timerallow-process-exit
const cleanup = new Cron('@hourly', {
unref: true,
catch: console.error,
}, () => removeExpiredEntries())unref is supported in Node and Deno, not browsers, and it means the process may exit before the next scheduled run.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| node-cron | npm | You want a widely recognized Node-only API and only need conventional cron scheduling |
| cron | npm | You need a mature Node scheduler with CronTime and explicit timezone support |
| node-schedule | npm | You prefer date-based recurrence rules and one-off jobs alongside cron expressions |