RRule vs Cron
Cron has been the default scheduling primitive for 50 years. RRule (RFC 5545) is a different model — one built around expressibility, determinism, and human readability. This page outlines the practical differences.
At a glance
| Capability | Cron | RRule (RFC 5545) |
|---|---|---|
| True elapsed interval semantics | ⚠️ field-based only | ✅ |
| Specific day of week | ✅ | ✅ |
| Every N weeks | ⚠️ workaround | ✅ |
| Nth weekday of month (e.g. first Monday) | ❌ | ✅ |
| Last day of month | ❌ | ✅ |
| Run exactly N times (COUNT) | ❌ | ✅ |
| Run until a date (UNTIL) | ❌ | ✅ |
| Explicit IANA timezone | ❌ | ✅ |
| Automatic DST handling | ❌ | ✅ |
| Human-readable explanation | ❌ | ✅ |
| Deterministic simulation | ❌ | ✅ |
| Standardized format (RFC) | ❌ | ✅ |
Expressiveness
Cron uses five fields (minute, hour, day-of-month, month, day-of-week) and supports basic intervals via */n syntax. This covers a narrow range of real-world schedules. Anything more complex either requires workarounds or simply cannot be expressed.
Every two weeks
Cron
# No native support.
# Common workaround: run weekly, check week number in script
0 9 * * 1
# Requires external logic to skip odd/even weeksRRule
FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;BYHOUR=9;BYMINUTE=0First Monday of every month
Cron
# Not expressible in standard cron.
# Workaround: run every Monday, filter with script
0 9 * * 1
# if [ $(date +\%e) -le 7 ]; then ...RRule
FREQ=MONTHLY;BYDAY=+1MO;BYHOUR=9;BYMINUTE=0Last day of every month
Cron
# Not expressible. Common workaround uses a wrapper:
0 9 28-31 * *
# Requires: [ "$(date +%d)" = "$(cal | awk '/[0-9]/{x=$NF} END{print x}')" ]RRule
FREQ=MONTHLY;BYMONTHDAY=-1;BYHOUR=9;BYMINUTE=0BYMONTHDAY=-1 means the last day of the month, regardless of its length.
Every weekday, 10 times then stop
Cron
# COUNT not supported. Requires external counter:
0 9 * * 1-5
# Script must track executions and remove crontab after 10 runsRRule
FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0;COUNT=10Why cron step syntax is misleading
Cron step syntax such as */25 looks like an interval, but it is really a field matcher. In the minutes field, it means "run when minute is 0, 25, or 50" — not "run every 25 minutes forever".
That difference becomes visible as soon as the field rolls over. The pattern resets at the next hour instead of continuing with a true 25-minute interval.
Cron
# Cron expression
*/25 * * * *
# Matching minutes in each hour:
# 16:25
# 16:50
# 17:00 ← field reset
# 17:25
# 17:50True interval semantics
# What most people intuitively expect from
# "every 25 minutes":
# 16:25
# 16:50
# 17:15
# 17:40
# 18:05This is not limited to minutes. It comes from the cron model itself: each field matches allowed calendar values, rather than expressing elapsed time since the previous occurrence.
Bounded schedules
Cron jobs run indefinitely by default. Stopping them requires external logic: a wrapper script, a database flag, or removing the crontab entry. There is no native way to say "run 10 times" or "run until March 31st".
RRule has first-class support for both:
# Run exactly 10 times, then stop
FREQ=WEEKLY;BYDAY=MO;BYHOUR=9;COUNT=10
# Run every week until March 31st
FREQ=WEEKLY;BYDAY=MO;BYHOUR=9;UNTIL=20261231T000000ZThe schedule is self-describing: its end condition is part of the rule itself, not external state.
Timezones and DST
Standard cron has no timezone concept. It runs in the system timezone of the host. When the host changes timezone, or when DST transitions occur, behavior shifts silently. A job scheduled at 0 9 * * * may fire at 8am or 10am after a DST transition depending on the implementation.
RRule is paired with an explicit IANA timezone. The question "9am means 9am local time, always" is answered unambiguously:
# Every weekday at 9am, expressed with explicit timezone
DTSTART;TZID=America/New_York:20260101T090000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0
# rrule.net output in UTC (automatic DST handling):
# 2026-03-09T14:00:00Z → 9am EST (UTC-5)
# 2026-03-16T13:00:00Z → 9am EDT (UTC-4) ← DST transition handledThe UTC offsets shift automatically at DST boundaries. The local time stays constant.
Explainability
Given a cron expression, the only way to understand it is to parse it manually or use a third-party tool. There is no standard for producing a human-readable description from a cron string.
RRule expressions can be described in natural language. rrule.net goes further: validation returns an explanation alongside canonical recurrence JSON, so both humans and AI agents can verify intent before anything is scheduled.
rrule.net — POST /v1/schedules/validate
// Input
{
"input": "Every second Tuesday of the month at 6pm",
"timezone": "Europe/Paris"
}
// Output
{
"valid": true,
"input": {
"type": "natural",
"value": "Every second Tuesday of the month at 6pm",
"language": "en"
},
"recurrence": {
"kind": "input",
"start": "2026-02-10T18:00:00+01:00[Europe/Paris]",
"tzid": "Europe/Paris",
"include": [
{
"rule": {
"freq": "MONTHLY",
"byDay": ["TU"],
"bySetPos": [2],
"byHour": [18],
"byMinute": [0],
"bySecond": [0]
}
}
],
"exclude": []
},
"explanation": {
"text": "Every second Tuesday of the month at 6:00 PM (Europe/Paris)",
"confidence": 0.97
}
}The confidence score indicates how certain the parser is about the interpretation. Ambiguous inputs are rejected with explicit clarification questions rather than silently guessed.
Determinism and simulation
A cron expression cannot be simulated without running against a clock. You can mentally parse the fields, but verifying that it does what you intend on specific edge cases (end of month, DST, leap years) requires a test environment.
A recurrence schedule is a pure mathematical function: given a start date and a rule, the full sequence of occurrences is deterministic and computable without side effects. This makes it trivial to preview, test, and audit. rrule.net keeps RRule as a useful input format, then stores the canonical recurrence JSON model.
// rrule.net — POST /v1/schedules/simulate
{
"input": "FREQ=MONTHLY;BYDAY=TU;BYSETPOS=2;BYHOUR=18;BYMINUTE=0",
"timezone": "Europe/Paris",
"count": 5
}
// Response
{
"occurrences": [
"2026-02-10T17:00:00.000Z",
"2026-03-10T17:00:00.000Z",
"2026-04-14T16:00:00.000Z", // DST: Paris moves to UTC+2
"2026-05-12T16:00:00.000Z",
"2026-06-09T16:00:00.000Z"
]
}Standardization
Cron has no single specification. Vixie cron, systemd timers, AWS EventBridge, Google Cloud Scheduler, and GitHub Actions each implement slightly different syntax. A cron expression that works in one context may be invalid or behave differently in another.
RRule is defined by RFC 5545 (iCalendar, 1998) and its successor RFC 7986. The same rule parsed by any conforming implementation produces the same result. It is the format used internally by Google Calendar, Outlook, and Apple Calendar to store recurring events.
When to use each
Cron is not a bad tool. It is a simple, battle-tested tool for simple cases. The friction starts when your requirements grow beyond its expressive range.
Cron is appropriate for
- ✓ Simple periodic system tasks ("every 5 minutes")
- ✓ Infrastructure jobs with no business logic dependency
- ✓ Single-server environments in a fixed timezone
- ✓ Jobs that run indefinitely with no end condition
RRule is appropriate for
- ✓ User-defined schedules (billing, reminders, reports)
- ✓ Multi-timezone SaaS with international users
- ✓ Calendar-aware patterns (last day of month, nth weekday)
- ✓ Bounded schedules (COUNT or UNTIL)
- ✓ Contexts requiring auditing and explainability
- ✓ AI agents that need to reason about time
rrule.net exposes recurrence validation, simulation, and scheduling as a REST API, with RRule, cron, natural language input, and timezone-aware DST handling.