Safeguard
Open Source

rrule npm Package: Recurrence Rules, Health, and Pitfalls

The rrule npm package is the standard way to handle iCalendar recurrence rules in JavaScript — but it carries timezone traps, unbounded-expansion hazards, and a slow maintenance pulse worth knowing before you depend on it.

Karan Patel
Platform Engineer
7 min read

The rrule npm package is the de facto JavaScript implementation of RFC 5545 recurrence rules — "every second Tuesday until June" as a parseable, expandable object — and it is good enough that FullCalendar builds its recurrence plugin on it, but it ships with timezone semantics and denial-of-service footguns that bite almost everyone once. This review covers what the library does well, where its maintenance stands, and the specific pitfalls to engineer around before recurrence data from users starts flowing through it.

What rrule does

Calendar recurrence is deceptively hard. The iCalendar spec (RFC 5545) defines a grammar — FREQ, INTERVAL, BYDAY, COUNT, UNTIL and friends — that can express schedules like "the last weekday of every month" in one line. rrule, a JavaScript port in the spirit of Python's dateutil.rrule, parses that grammar and expands it into concrete dates:

import { RRule } from "rrule";

const rule = new RRule({
  freq: RRule.WEEKLY,
  byweekday: [RRule.TU],
  interval: 2,
  dtstart: new Date(Date.UTC(2025, 1, 4, 9, 0, 0)),
  count: 10,
});

rule.all();          // ten concrete Date objects
rule.toString();     // "DTSTART:20250204T090000Z\nRRULE:FREQ=WEEKLY;..."
RRule.fromString("FREQ=DAILY;COUNT=3"); // round-trips

It also offers between(after, before), after(date), RRuleSet for combining rules with exceptions, and natural-language output via toText(). If you are building scheduling, booking, or calendar-sync features, this is the coverage you want and do not want to hand-roll.

Package health: stable, slow, and worth watching

The current release is 2.8.1, and it has been the current release for years — the package has had no new npm versions in well over twelve months, which dependency-health trackers classify as low-activity or potentially discontinued. The GitHub repository (jkbrzt/rrule) carries a long tail of open issues, many of them the same few timezone and daylight-saving reports in different clothing.

Context matters when reading that signal. rrule implements a frozen specification; RFC 5545 is not growing features, so a quiet library is partly a finished library. There are no published security advisories against it in the mainstream databases. But "no CVEs and no releases" also means that if you hit one of the known DST bugs, the realistic remediation is a workaround in your code, not a patch from upstream. Factor that into how central you let it become — the calculus we describe for abandoned open source project risks applies in the milder "sustainable but slow" form here.

Pitfall 1: the timezone model is not what you assume

rrule's dates are best understood as floating times wearing UTC clothing. The library deliberately ignores JavaScript's local-timezone behavior; the README tells you to construct inputs as UTC-based Date objects (and provides a datetime() helper — note its months are 1-based, unlike Date.UTC) and to interpret outputs the same way. Feed it new Date(2025, 1, 4, 9) — a local-time construction — and you will get offsets silently baked into every expanded occurrence.

Daylight saving is the second layer. A human expects "every Monday at 09:00 New York time" to stay at 09:00 through a DST transition. Naive UTC arithmetic shifts it an hour, and the repository's issue tracker documents exactly this failure mode in long-running recurrences. rrule has tzid support for expressing zone-aware rules, but the safe pattern in practice is:

  • Store dtstart and the rule in UTC plus an explicit IANA zone name for display and business logic.
  • Expand in UTC, then convert each occurrence to the target zone with a real timezone library (Luxon, date-fns-tz) at the edge.
  • Write regression tests that cross a DST boundary in both directions. This one habit catches the majority of recurrence bugs before users do.

Pitfall 2: unbounded expansion is a self-inflicted DoS

An RRULE without COUNT or UNTIL is infinite by design — "every day, forever" is valid iCalendar. That turns two innocent-looking calls into hazards:

  • rule.all() on an infinite rule will iterate until it exhausts memory or your patience, unless you pass the iterator callback to stop it.
  • rule.between(a, b) over a wide window on a high-frequency rule (think FREQ=SECONDLY) generates enormous arrays.

If users or external calendars (ICS imports are the classic vector) can supply rules, treat the rule string as untrusted input:

const MAX_OCCURRENCES = 1000;
const dates = rule.all((date, i) => i < MAX_OCCURRENCES);

Additionally cap the expansion window server-side (nobody needs ten years of occurrences in one response), reject FREQ=SECONDLY/MINUTELY unless you genuinely support them, and parse inside a try/catch — fromString throws on malformed grammar, and malformed grammar is exactly what fuzzed ICS files contain. The same "attacker-shaped input reaching an expensive engine" reasoning that applies to regex engines applies to recurrence expansion; a DAST scan that replays ICS-import endpoints with hostile payloads is a decent way to confirm your caps actually hold in production paths.

Pitfall 3: serialization round-trips and cross-system drift

RRULE strings travel — to Google Calendar, Outlook, other services — and implementations disagree on edge cases (notably UNTIL inclusivity and BYDAY corner combinations across DST). If your product syncs recurrences with external calendars, round-trip your own serialization (fromString(toString(rule))) in tests and keep golden-file fixtures for schedules that matter commercially. Silent drift of one occurrence per year is the kind of bug that surfaces as a missed customer meeting, not a stack trace.

Alternatives and when to pick them

  • rrule (this package): best RFC 5545 coverage in pure JS; accept the slow cadence and wrap the pitfalls.
  • rrule-rust and similar native bindings: faster expansion for server-heavy workloads, at the cost of native-module operational overhead.
  • Storing materialized occurrences: for bounded schedules (a 12-session course), expanding once at write time and storing plain rows sidesteps the entire library category — often the right call.
  • A hosted calendar API: if recurrence is incidental to your product, offloading RFC 5545 to a provider outsources the edge cases along with the parsing.

For most teams, rrule plus disciplined input caps and DST tests remains the pragmatic default, and pinning it exactly in your lockfile costs nothing given the release tempo. Keep it visible in your dependency inventory with the rest of your third-party surface — quiet packages are the ones that go stale unnoticed, which is a workflow problem before it is a security one; see our 10 npm security best practices for the inventory habits that make it automatic.

FAQ

Is the rrule npm package still maintained?

It is in low-activity maintenance: version 2.8.1 has been current for several years with no fresh releases, though the project has no published security advisories. Treat it as stable-but-slow and expect to work around known timezone issues rather than wait for fixes.

Why do my rrule occurrences shift by an hour after a DST change?

Because expansion is effectively UTC/floating arithmetic while your users think in a local zone. Store an explicit IANA timezone alongside the rule, expand in UTC, and convert per-occurrence at display time with a timezone-aware library; add tests that span DST boundaries.

Can a user-supplied RRULE crash my server?

An infinite rule expanded with all() or a wide between() can exhaust memory or peg CPU. Always cap occurrences with the iterator callback, bound the expansion window, restrict allowed frequencies, and wrap parsing in error handling before accepting rules from ICS imports or API clients.

What is the difference between COUNT and UNTIL?

COUNT limits a rule to a fixed number of occurrences; UNTIL bounds it by a timestamp. A rule with neither is infinite by specification, which is legal iCalendar — so your application layer, not the library, has to impose the safety limits.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.