rrule vs cron-parser vs later: Recurrence Rule Parsing (2026)
TL;DR
rrule.js parses and generates iCalendar RRULE recurrence rules — "every Tuesday and Thursday at 10am", "first Monday of every month", complex calendar recurrences with exclusions and timezones. cron-parser parses cron expressions — */5 * * * *, validates syntax, calculates next/previous occurrences, supports seconds and timezones. later defines schedules with a readable API — schedule definitions, composite schedules, exception handling, text parsing. In 2026: rrule.js for calendar-style recurrences (RRULE), cron-parser for cron expression parsing, later for readable schedule definitions.
Key Takeaways
- rrule.js: ~800K weekly downloads — iCalendar RRULE, calendar recurrences, exclusions
- cron-parser: ~10M weekly downloads — cron expressions, next/prev occurrence, validation
- later: ~1M weekly downloads — schedule definitions, text parsing, composite schedules
- rrule.js follows the iCalendar RFC 5545 standard
- cron-parser is the most popular — cron is the universal scheduling language
- later has the most readable schedule definition syntax
rrule.js
rrule.js — iCalendar recurrence rules:
Basic rules
import { RRule, RRuleSet, rrulestr } from "rrule"
// Every weekday at 10am:
const rule = new RRule({
freq: RRule.WEEKLY,
byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
byhour: [10],
byminute: [0],
dtstart: new Date(Date.UTC(2026, 0, 1)),
})
// Get next 5 occurrences:
const dates = rule.all((_, i) => i < 5)
console.log(dates)
// [Mon Jan 5, Tue Jan 6, Wed Jan 7, Thu Jan 8, Fri Jan 9]
// Human-readable text:
console.log(rule.toText())
// "every week on Monday, Tuesday, Wednesday, Thursday, Friday at 10:00"
// To RRULE string:
console.log(rule.toString())
// "RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=10;BYMINUTE=0"
Complex recurrences
import { RRule } from "rrule"
// First Monday of every month:
const firstMonday = new RRule({
freq: RRule.MONTHLY,
byweekday: [RRule.MO.nth(1)],
dtstart: new Date(Date.UTC(2026, 0, 1)),
})
// Every other week on Tuesday and Thursday:
const biweekly = new RRule({
freq: RRule.WEEKLY,
interval: 2,
byweekday: [RRule.TU, RRule.TH],
dtstart: new Date(Date.UTC(2026, 0, 1)),
})
// Last Friday of every quarter:
const quarterly = new RRule({
freq: RRule.MONTHLY,
interval: 3,
byweekday: [RRule.FR.nth(-1)],
dtstart: new Date(Date.UTC(2026, 0, 1)),
})
// Until a specific date:
const limited = new RRule({
freq: RRule.DAILY,
dtstart: new Date(Date.UTC(2026, 0, 1)),
until: new Date(Date.UTC(2026, 11, 31)),
})
// Count-limited:
const counted = new RRule({
freq: RRule.WEEKLY,
count: 10,
byweekday: [RRule.MO],
dtstart: new Date(Date.UTC(2026, 0, 1)),
})
RRuleSet (exclusions and additions)
import { RRule, RRuleSet } from "rrule"
const set = new RRuleSet()
// Base rule — every weekday:
set.rrule(new RRule({
freq: RRule.WEEKLY,
byweekday: [RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR],
dtstart: new Date(Date.UTC(2026, 0, 1)),
}))
// Exclude holidays:
set.exdate(new Date(Date.UTC(2026, 0, 1))) // New Year's Day
set.exdate(new Date(Date.UTC(2026, 6, 4))) // July 4th
set.exdate(new Date(Date.UTC(2026, 11, 25))) // Christmas
// Exclude date range (vacation):
set.exrule(new RRule({
freq: RRule.DAILY,
dtstart: new Date(Date.UTC(2026, 7, 1)),
until: new Date(Date.UTC(2026, 7, 15)),
}))
// Add specific extra dates:
set.rdate(new Date(Date.UTC(2026, 0, 3))) // Saturday makeup day
// Get occurrences:
const dates = set.between(
new Date(Date.UTC(2026, 0, 1)),
new Date(Date.UTC(2026, 1, 1)),
)
Parse RRULE strings
import { rrulestr } from "rrule"
// Parse from iCalendar string:
const rule = rrulestr("RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;BYHOUR=9;BYMINUTE=0")
console.log(rule.toText())
// "every week on Monday, Wednesday, Friday at 9:00"
console.log(rule.all((_, i) => i < 3))
// Next 3 occurrences
// Parse with DTSTART:
const ruleWithStart = rrulestr(
"DTSTART:20260101T090000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1"
)
// Parse full iCalendar set:
const ruleSet = rrulestr(
"DTSTART:20260101T090000Z\n" +
"RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\n" +
"EXDATE:20260106T090000Z",
{ forceset: true }
)
cron-parser
cron-parser — cron expression parsing:
Basic parsing
import parser from "cron-parser"
// Parse cron expression:
const interval = parser.parseExpression("*/5 * * * *") // Every 5 minutes
// Next occurrence:
console.log(interval.next().toDate())
// 2026-03-09T10:05:00.000Z
// Previous occurrence:
console.log(interval.prev().toDate())
// 2026-03-09T09:55:00.000Z
// Iterate next 5:
for (let i = 0; i < 5; i++) {
console.log(interval.next().toDate())
}
// Reset iterator:
interval.reset()
Common expressions
import parser from "cron-parser"
// Standard cron (5 fields: min hour dom month dow):
parser.parseExpression("0 9 * * *") // Every day at 9am
parser.parseExpression("0 9 * * 1-5") // Weekdays at 9am
parser.parseExpression("0 0 1 * *") // First of every month
parser.parseExpression("0 0 * * 0") // Every Sunday midnight
parser.parseExpression("30 8 * * 1") // Monday 8:30am
parser.parseExpression("0 */2 * * *") // Every 2 hours
parser.parseExpression("0 9,17 * * *") // 9am and 5pm
// With seconds (6 fields):
parser.parseExpression("*/30 * * * * *") // Every 30 seconds
parser.parseExpression("0 0 9 * * 1-5") // Weekdays at 9am (with seconds)
Options and timezones
import parser from "cron-parser"
// With timezone:
const interval = parser.parseExpression("0 9 * * *", {
tz: "America/New_York",
})
console.log(interval.next().toDate()) // 9am ET
// With date range:
const bounded = parser.parseExpression("0 9 * * *", {
currentDate: new Date("2026-03-01"),
startDate: new Date("2026-03-01"),
endDate: new Date("2026-03-31"),
tz: "America/Los_Angeles",
})
// Iterate within range:
try {
while (true) {
console.log(bounded.next().toDate())
}
} catch (e) {
// "Out of the timespan range" when past endDate
}
// With iterator:
const iter = parser.parseExpression("0 9 * * 1-5", {
iterator: true,
})
const result = iter.next()
console.log(result.value.toDate())
console.log(result.done) // false
Validation
import parser from "cron-parser"
// Validate expression:
function isValidCron(expression: string): boolean {
try {
parser.parseExpression(expression)
return true
} catch {
return false
}
}
console.log(isValidCron("*/5 * * * *")) // true
console.log(isValidCron("0 9 * * 1-5")) // true
console.log(isValidCron("invalid")) // false
console.log(isValidCron("60 * * * *")) // false (minutes 0-59)
// Get fields:
const fields = parser.fieldsToExpression(
parser.parseExpression("0 9 * * 1-5").fields
)
console.log(fields.stringify()) // "0 9 * * 1-5"
later
later — schedule definitions:
Basic schedules
import later from "@breejs/later"
// Parse text:
const schedule = later.parse.text("every 5 minutes")
// Next occurrence:
const next = later.schedule(schedule).next(1)
console.log(next)
// Next 5 occurrences:
const next5 = later.schedule(schedule).next(5)
console.log(next5)
// Previous occurrence:
const prev = later.schedule(schedule).prev(1)
console.log(prev)
Text parser
import later from "@breejs/later"
// Readable schedule definitions:
later.parse.text("every weekday at 9:00 am")
later.parse.text("every Monday at 10:00 am")
later.parse.text("on the first day of every month")
later.parse.text("every 30 minutes")
later.parse.text("at 9:00 am and 5:00 pm")
later.parse.text("on the last Friday of every month")
later.parse.text("every 2nd hour")
// Parse cron too:
later.parse.cron("0 9 * * 1-5") // Standard cron
later.parse.cron("0 0 9 * * 1-5") // With seconds
Recurrence builder
import later from "@breejs/later"
// Programmatic schedule definition:
const weekdays9am = later.parse.recur()
.on(2, 3, 4, 5, 6).dayOfWeek() // Mon-Fri (1=Sun)
.on("09:00").time()
// Every 15 minutes during business hours:
const businessHours = later.parse.recur()
.every(15).minute()
.after("09:00").time()
.before("17:00").time()
.on(2, 3, 4, 5, 6).dayOfWeek()
// First and fifteenth of each month:
const payDays = later.parse.recur()
.on(1, 15).dayOfMonth()
.on("09:00").time()
// Get occurrences:
const sched = later.schedule(weekdays9am)
const nextDates = sched.next(5, new Date("2026-03-01"))
console.log(nextDates)
Composite schedules (AND/OR/EXCEPT)
import later from "@breejs/later"
// Composite — multiple schedules OR'd together:
const composite = {
schedules: [
// Every weekday at 9am:
later.parse.recur()
.on(2, 3, 4, 5, 6).dayOfWeek()
.on("09:00").time(),
// Also every Saturday at noon:
later.parse.recur()
.on(7).dayOfWeek()
.on("12:00").time(),
],
exceptions: [
// Except holidays (first of each month as example):
later.parse.recur()
.on(1).dayOfMonth(),
],
}
const sched = later.schedule(composite)
const dates = sched.next(10, new Date("2026-03-01"))
Set timezone
import later from "@breejs/later"
// Use local time (default is UTC):
later.date.localTime()
// Use UTC:
later.date.UTC()
// Execute on schedule:
const schedule = later.parse.text("every 5 minutes")
const timer = later.setInterval(() => {
console.log("Running task at:", new Date())
}, schedule)
// Stop:
timer.clear()
// Set timeout for next occurrence:
const timeout = later.setTimeout(() => {
console.log("Next occurrence!")
}, schedule)
timeout.clear()
Feature Comparison
| Feature | rrule.js | cron-parser | later |
|---|---|---|---|
| Format | iCalendar RRULE | Cron expressions | Text/recurrence API |
| Standard | RFC 5545 | POSIX cron | Custom |
| Next occurrence | ✅ | ✅ | ✅ |
| Previous occurrence | ✅ | ✅ | ✅ |
| Date range query | ✅ (between) | ✅ (startDate/endDate) | ✅ (next with start) |
| Exclusions | ✅ (RRuleSet) | ❌ | ✅ (exceptions) |
| Human-readable text | ✅ (toText) | ❌ | ✅ (parse.text) |
| Timezone support | ✅ (tzid) | ✅ (tz option) | ✅ (local/UTC) |
| Seconds precision | ❌ | ✅ (6-field) | ✅ |
| Built-in timer | ❌ | ❌ | ✅ (setInterval) |
| Composite schedules | ✅ (RRuleSet) | ❌ | ✅ |
| Calendar recurrences | ✅ (nth weekday) | ❌ | ✅ (limited) |
| TypeScript | ✅ | ✅ | ✅ (@breejs/later) |
| Weekly downloads | ~800K | ~10M | ~1M |
When to Use Each
Use rrule.js if:
- Need iCalendar RRULE standard compliance
- Building calendar or scheduling apps with complex recurrences
- Need "first Monday of every month" or "every other Tuesday" patterns
- Want exclusion dates and RRuleSet combinations
Use cron-parser if:
- Working with cron expressions (the universal scheduling format)
- Need to validate and parse cron syntax
- Building job schedulers or task runners
- Want the most widely adopted solution
Use later if:
- Want human-readable schedule definitions
- Need composite schedules with exceptions
- Want built-in setInterval/setTimeout for schedule execution
- Prefer a fluent API over string-based formats
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on rrule v2.x, cron-parser v4.x, and @breejs/later v4.x.
Compare scheduling libraries and backend tooling on PkgPulse →