Recurring events in ICS: RRULE basics without the headache

·By Elysiate·Updated Apr 10, 2026·
icsicalrrulecalendarrecurring-eventsdeveloper-tools
·

Level: intermediate · ~15 min read · Intent: informational

Audience: developers, ops engineers, product teams, technical teams

Prerequisites

  • basic familiarity with ICS files
  • basic understanding of calendar events

Key takeaways

  • RRULE does not stand alone. DTSTART anchors the series, and EXDATE, RDATE, and RECURRENCE-ID often matter just as much as the rule itself.
  • The safest recurring-event approach is to keep rules simple when possible: choose the right FREQ, add INTERVAL only when needed, and avoid overcomplicated BY* combinations unless you have test coverage.
  • COUNT and UNTIL are not interchangeable. COUNT caps the number of instances, while UNTIL sets an end boundary that can behave differently depending on date-time format and time zone handling.
  • Time zones and DST are where many recurring-event bugs come from. Floating times, UTC times, and TZID-based local times can produce very different outcomes across clients.

References

FAQ

What is RRULE in an ICS file?
RRULE is the recurrence rule property in iCalendar. It defines how an event repeats, such as daily, weekly, monthly, or yearly, and can be combined with options like INTERVAL, COUNT, UNTIL, and BYDAY.
What is the difference between COUNT and UNTIL in RRULE?
COUNT limits the number of generated occurrences, while UNTIL defines the latest boundary for occurrences. COUNT is instance-based. UNTIL is date-based and needs careful handling with time zones and date-time format.
How do I exclude one occurrence from a recurring ICS event?
Use EXDATE to remove a specific occurrence from the recurring set. If you also need to replace that instance with a changed one, pair the exception with a separate VEVENT using RECURRENCE-ID.
Why do recurring events import differently across calendar apps?
Calendar clients differ in how they handle time zones, custom recurrence combinations, exceptions, and imported ICS files. The more complex the rule, the more important it is to test in Google, Apple, and Outlook-style clients.
0

Recurring events in ICS: RRULE basics without the headache

Recurring events in ICS files look simple until they stop being simple.

At first, most teams only need something like:

  • repeat every week
  • repeat every month
  • stop after 10 occurrences
  • skip one holiday

Then the requirements expand:

  • repeat every second Tuesday and Thursday
  • run at 9 AM local time in the user’s time zone
  • keep the meeting at 9 AM even after daylight saving changes
  • move only one occurrence without rewriting the whole series
  • import cleanly into Google Calendar, Apple Calendar, and Outlook-style clients

That is where RRULE starts to feel more complicated than it looked.

The good news is that most recurring-event problems come from a small number of ideas:

  • how DTSTART anchors the series
  • how RRULE generates the base pattern
  • how COUNT and UNTIL stop the pattern
  • how EXDATE, RDATE, and RECURRENCE-ID modify the generated set
  • how time zones and DST change what “same time” actually means

This guide explains those pieces in plain English, with examples you can actually adapt.

If you are building calendar features, event reminders, meeting exports, or downloadable ICS files, this page should help you avoid the most common recurrence mistakes.

Why RRULE feels harder than it should

RRULE is powerful, but it lives inside a bigger iCalendar model.

That means a recurring event is usually not just one property. It is a combination of:

  • BEGIN:VEVENT
  • UID
  • DTSTART
  • DTEND or DURATION
  • RRULE
  • optional EXDATE
  • optional RDATE
  • optional modified instance events using RECURRENCE-ID

A lot of developer frustration comes from treating RRULE as the whole story. It is not.

A better mental model is:

  1. DTSTART sets the anchor.
  2. RRULE describes the repeating pattern.
  3. EXDATE removes instances.
  4. RDATE adds specific instances.
  5. RECURRENCE-ID lets you override one generated occurrence.

Once you think in recurrence sets instead of one property string, the format becomes easier to reason about.

The minimum recurring ICS event

Here is a simple weekly example:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Elysiate//ICS Guide//EN
BEGIN:VEVENT
UID:weekly-demo-001@elysiate.com
DTSTAMP:20260424T080000Z
DTSTART:20260427T090000Z
DTEND:20260427T093000Z
SUMMARY:Weekly demo
RRULE:FREQ=WEEKLY
END:VEVENT
END:VCALENDAR

This means:

  • the first instance starts on 27 April 2026 at 09:00 UTC
  • the event repeats weekly
  • the event length is 30 minutes

This is the best place to start.

If you can express the recurrence you need with a simple FREQ, do that first. The more BY* parts you add, the more client behavior you need to test.

The RRULE parts that matter most

FREQ

FREQ is the backbone of the rule.

Common values include:

  • DAILY
  • WEEKLY
  • MONTHLY
  • YEARLY

Examples:

RRULE:FREQ=DAILY
RRULE:FREQ=WEEKLY
RRULE:FREQ=MONTHLY
RRULE:FREQ=YEARLY

Pick the simplest frequency that matches the business intent. Do not use a complex monthly rule when a weekly rule would describe the behavior more clearly.

INTERVAL

INTERVAL tells the calendar how often to apply the frequency.

Examples:

RRULE:FREQ=DAILY;INTERVAL=2

This means every 2 days.

RRULE:FREQ=WEEKLY;INTERVAL=2

This means every 2 weeks.

RRULE:FREQ=MONTHLY;INTERVAL=3

This means every 3 months.

A lot of recurring schedules can be handled with just FREQ and INTERVAL. That is one reason to prefer them whenever possible.

COUNT

COUNT limits the number of generated instances.

Example:

RRULE:FREQ=WEEKLY;COUNT=10

This means generate 10 occurrences total, including the first one anchored by DTSTART.

Use COUNT when the business requirement is instance-based:

  • a 6-session course
  • a 10-week program
  • a 12-call interview series

COUNT is usually easier to reason about than UNTIL when you care about a fixed number of occurrences.

UNTIL

UNTIL sets a boundary instead of a count.

Example:

RRULE:FREQ=WEEKLY;UNTIL=20260731T235959Z

This means repeat weekly until the recurrence boundary is reached.

Use UNTIL when the business requirement is date-based:

  • repeat until the end of the quarter
  • repeat until the project closes
  • repeat until a semester ends

This is one of the biggest sources of confusion in ICS recurrence.

COUNT vs UNTIL

Use COUNT when you know how many. Use UNTIL when you know until when.

Do not treat them as interchangeable.

They can produce different results when:

  • time zones are involved
  • DST changes are involved
  • the rule skips invalid dates
  • the last date lands differently than you expected

A safer default is:

  • choose COUNT for finite class-like schedules
  • choose UNTIL for calendar-bound schedules

BYDAY

BYDAY is one of the most common rule parts after FREQ.

Examples:

RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR

This means every Monday, Wednesday, and Friday.

RRULE:FREQ=MONTHLY;BYDAY=TU

This means the recurrence is monthly, filtered by Tuesday behavior, which often needs another constraint such as BYSETPOS to match human intent.

The important weekly case

For weekly rules, BYDAY is usually straightforward:

BEGIN:VEVENT
UID:fitness-class-001@elysiate.com
DTSTAMP:20260424T080000Z
DTSTART:20260428T180000Z
DTEND:20260428T190000Z
SUMMARY:Fitness class
RRULE:FREQ=WEEKLY;BYDAY=TU,TH
END:VEVENT

This is a good pattern for:

  • classes
  • staff shifts
  • office hours
  • recurring reminders

BYMONTHDAY

BYMONTHDAY is for dates like the 1st, 15th, or 31st of the month.

Example:

RRULE:FREQ=MONTHLY;BYMONTHDAY=15

This means the 15th of each month.

This is useful for:

  • billing reminders
  • rent notices
  • payroll checkpoints
  • monthly reporting deadlines

But it comes with an important edge case.

Invalid dates do not magically become other dates

If you use:

RRULE:FREQ=MONTHLY;BYMONTHDAY=31

then months without a 31st do not create a substitute occurrence like the 30th.

That is one reason “monthly on the last day of the month” is not the same thing as “monthly on day 31.”

BYSETPOS

BYSETPOS is where recurrence rules start to feel advanced.

It is commonly used for patterns like:

  • first Monday of the month
  • second Tuesday of the month
  • last weekday of the month

Example:

RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1

This means the first Monday of the month.

Example:

RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1

This means the last weekday of the month.

These rules are powerful, but they are also the ones most likely to deserve client testing before you trust them in production.

Common recurring-event examples

Every weekday at 9 AM

BEGIN:VEVENT
UID:weekday-standup-001@elysiate.com
DTSTAMP:20260424T080000Z
DTSTART:20260427T090000Z
DTEND:20260427T091500Z
SUMMARY:Weekday standup
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
END:VEVENT

Every two weeks on Tuesday

RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU

Monthly on the 15th

RRULE:FREQ=MONTHLY;BYMONTHDAY=15

First Monday of every month

RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=1

Last Friday of every month

RRULE:FREQ=MONTHLY;BYDAY=FR;BYSETPOS=-1

Yearly on 1 January

RRULE:FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1

These are the kinds of examples that cover a large percentage of real product requirements.

DTSTART matters more than many people expect

A recurring rule does not float independently. It is anchored by DTSTART.

That matters because DTSTART influences:

  • the first instance
  • the time of day
  • whether the event is timed or all-day
  • the time zone context

For example, this is an all-day recurring event:

DTSTART;VALUE=DATE:20260501
RRULE:FREQ=DAILY;COUNT=5

This is not the same as a timed UTC event:

DTSTART:20260501T000000Z
RRULE:FREQ=DAILY;COUNT=5

If your event should behave like a local all-day holiday, use an all-day pattern. Do not fake it with midnight UTC unless that is truly what you want.

Time zones and DST are where recurrence gets real

This is where many “it works in one calendar but not another” issues begin.

There are three broad timing models you need to think about:

UTC time

Example:

DTSTART:20260427T090000Z

This is a fixed moment in UTC. It is useful when the absolute instant matters more than local wall-clock time.

Local time with TZID

Example:

DTSTART;TZID=Africa/Johannesburg:20260427T090000

This means 9 AM in the named time zone. This is often the right choice when the business meaning is “9 AM local time.”

Floating local time

Example:

DTSTART:20260427T090000

This has no trailing Z and no explicit TZID. That means the value can be interpreted as a floating local time, which is often not what you want for cross-client interoperability.

Safer rule for product teams

Use:

  • UTC when the absolute instant matters
  • TZID when the local wall-clock meaning matters
  • floating time only when you truly understand the client behavior you want

DST edge cases

Recurring events around daylight saving shifts are notorious.

Questions you need to answer:

  • Should the event always happen at 9 AM local time, even after DST changes?
  • Or should it stay at one absolute UTC instant?
  • What should happen if the recurrence lands on a nonexistent or shifted local time?

If the human expectation is “every Tuesday at 9 AM in New York,” then a time-zone-based local event is usually the right model. If the expectation is “every 168 hours,” that is a different concept.

EXDATE: removing one or more occurrences

EXDATE removes specific instances from the generated recurrence set.

Example:

BEGIN:VEVENT
UID:team-sync-001@elysiate.com
DTSTAMP:20260424T080000Z
DTSTART:20260501T150000Z
DTEND:20260501T160000Z
SUMMARY:Team sync
RRULE:FREQ=WEEKLY;BYDAY=TH
EXDATE:20260529T150000Z
END:VEVENT

This means:

  • the event repeats weekly on Thursday
  • one specific Thursday is removed from the series

This is the right tool for:

  • holidays
  • skipped sessions
  • office closures
  • one-time cancellations

RDATE: adding an extra occurrence

RDATE lets you add explicit occurrences outside the base RRULE pattern.

Example:

RDATE:20260603T150000Z

This is useful when the recurring pattern is mostly stable, but you need one extra instance.

Think of it as:

  • RRULE generates
  • EXDATE subtracts
  • RDATE adds

RECURRENCE-ID: modifying one occurrence instead of just deleting it

This is one of the most important concepts for real calendar products.

If you need to move or edit only one instance in a series, you usually do not just change the master event. You create an exception event with the same UID and a RECURRENCE-ID pointing to the original occurrence.

Example pattern:

BEGIN:VEVENT
UID:office-hours-001@elysiate.com
DTSTAMP:20260424T080000Z
DTSTART:20260506T140000Z
DTEND:20260506T150000Z
SUMMARY:Office hours
RRULE:FREQ=WEEKLY;BYDAY=WE
END:VEVENT
BEGIN:VEVENT
UID:office-hours-001@elysiate.com
DTSTAMP:20260424T080000Z
RECURRENCE-ID:20260520T140000Z
DTSTART:20260520T160000Z
DTEND:20260520T170000Z
SUMMARY:Office hours (moved)
END:VEVENT

This says:

  • the base event repeats weekly on Wednesday
  • the 20 May 2026 occurrence is overridden and moved to a different time

This is the clean way to express:

  • one meeting moved
  • one instance retitled
  • one occurrence happening in a different room

Common RRULE mistakes

Mistake 1: Using very complex rules when a simpler one works

Simple rules are easier to debug, test, and import. If FREQ=WEEKLY;BYDAY=MO,WE works, do not replace it with a more complex monthly or positional rule.

Mistake 2: Treating COUNT like an end date

A rule that ends after 10 occurrences is not the same as a rule that ends on a certain calendar date. Pick the one that matches the product requirement.

Mistake 3: Forgetting that DTSTART defines more than the date

It also defines timing context. Time zone mistakes often come from using the wrong DTSTART form.

Mistake 4: Using floating times when you really need named time zones

Floating times can be tempting because they look simple. They can also become ambiguous across clients.

Mistake 5: Deleting one occurrence when you meant to replace it

If one instance needs to change, EXDATE alone is not enough. You often need an exception event with RECURRENCE-ID.

Mistake 6: Assuming all clients interpret advanced rules identically

The more complex the rule, the more important testing becomes. That is especially true for:

  • BYSETPOS combinations
  • complex monthly logic
  • time-zone edge cases
  • imported ICS files with exceptions

A safer authoring approach for ICS recurrence

If you are generating ICS files programmatically, use this sequence:

  1. Define the business meaning in plain language.
  2. Choose the simplest FREQ and INTERVAL that matches it.
  3. Add BYDAY or BYMONTHDAY only if needed.
  4. Use COUNT or UNTIL, but do not casually mix them.
  5. Decide whether timing should be UTC, TZID, or all-day date-based.
  6. Add EXDATE for skipped instances.
  7. Add RECURRENCE-ID exceptions for moved or edited single occurrences.
  8. Import and test in at least Google and Apple calendar environments.

That workflow catches a huge number of recurrence bugs before users ever see them.

What to test before shipping recurring ICS files

At minimum, test these cases:

  • weekly recurrence
  • monthly recurrence
  • all-day recurrence
  • recurrence with COUNT
  • recurrence with UNTIL
  • one EXDATE
  • one RECURRENCE-ID override
  • one rule spanning a DST boundary
  • one rule imported into Google Calendar
  • one rule imported into Apple Calendar

For product teams, recurrence bugs are often trust bugs. A single “wrong day” or “wrong time” issue can make users stop trusting calendar export entirely.

Good search-intent coverage for this page

This page is intentionally structured to rank for more than one RRULE phrase. It should also be relevant to searches like:

  • what is rrule in ics
  • how to create recurring events in ical
  • ics recurring event examples
  • exdate vs recurrence-id
  • count vs until in rrule
  • monthly recurring event ics example
  • weekly recurring calendar event ics
  • ics timezone recurring event problems
  • google calendar recurring ics import
  • apple calendar repeating event ics

That matters because recurrence questions are usually symptom-driven. People often search by the exact thing that broke, not by the spec term they need.

Which Elysiate tools fit this topic best?

The most natural supporting tool here is the ICS File Generator. It fits this article because recurring events are one of the fastest ways for hand-written calendar files to go wrong.

You can also pair this page with:

Final takeaway

RRULE gets easier once you stop treating it like a magic string.

A recurring ICS event is really:

  • an anchored start
  • a recurrence pattern
  • a stop condition
  • optional removals
  • optional additions
  • optional single-instance overrides

That is why the safest way to work with recurrence is:

  • keep the base rule simple
  • choose time handling deliberately
  • use EXDATE and RECURRENCE-ID correctly
  • test the result in real calendar clients

That approach is what turns RRULE from “calendar headache” into a stable export format.

FAQ

What is RRULE in an ICS file?

RRULE is the recurrence rule property in iCalendar. It defines how an event repeats, such as daily, weekly, monthly, or yearly, and can be combined with options like INTERVAL, COUNT, UNTIL, and BYDAY.

What is the difference between COUNT and UNTIL in RRULE?

COUNT limits the number of generated occurrences, while UNTIL defines the latest boundary for occurrences. COUNT is usually easier when you want a fixed number of sessions. UNTIL is better when the schedule ends on a known date.

How do I exclude one recurring instance from an ICS event?

Use EXDATE to remove a specific instance from the recurrence set. If the occurrence should be moved or modified rather than deleted, pair the exception with a separate VEVENT using RECURRENCE-ID.

Why do recurring ICS events behave differently across calendar apps?

Different clients make slightly different choices around time zones, recurrence expansion, imported files, and exception handling. The more advanced the rule, the more important cross-client testing becomes.

What is the safest default for recurring events in ICS?

Use the simplest rule that matches the business requirement, anchor it with a clear DTSTART, prefer explicit time-zone handling over ambiguity, and use EXDATE and RECURRENCE-ID instead of trying to force every exception into one giant RRULE.

References

About the author

Elysiate publishes practical guides and privacy-first tools for data workflows, developer tooling, SEO, and product engineering.

Free, privacy-first utilities in your browser — no uploads required for most workflows.

Related posts