Recurring events in ICS: RRULE basics without the headache
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.
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
DTSTARTanchors the series - how
RRULEgenerates the base pattern - how
COUNTandUNTILstop the pattern - how
EXDATE,RDATE, andRECURRENCE-IDmodify 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:VEVENTUIDDTSTARTDTENDorDURATIONRRULE- 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:
DTSTARTsets the anchor.RRULEdescribes the repeating pattern.EXDATEremoves instances.RDATEadds specific instances.RECURRENCE-IDlets 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:
DAILYWEEKLYMONTHLYYEARLY
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
COUNTfor finite class-like schedules - choose
UNTILfor 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
TZIDwhen 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:
RRULEgeneratesEXDATEsubtractsRDATEadds
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:
- Define the business meaning in plain language.
- Choose the simplest
FREQandINTERVALthat matches it. - Add
BYDAYorBYMONTHDAYonly if needed. - Use
COUNTorUNTIL, but do not casually mix them. - Decide whether timing should be UTC,
TZID, or all-day date-based. - Add
EXDATEfor skipped instances. - Add
RECURRENCE-IDexceptions for moved or edited single occurrences. - 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-IDoverride - 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:
- QR tools when you are embedding event links or calendar actions in QR flows
- SVG QR Generator for printable event materials
- Universal Converter for adjacent file-format workflows
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
EXDATEandRECURRENCE-IDcorrectly - 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
-
RFC 5545 — Internet Calendaring and Scheduling Core Object Specification
https://www.rfc-editor.org/rfc/rfc5545 -
RFC 5545 errata
https://www.rfc-editor.org/errata/rfc5545 -
Google Calendar API — recurring events
https://developers.google.com/workspace/calendar/api/guides/recurringevents -
Google Calendar Help — import events
https://support.google.com/calendar/answer/37118 -
Google Calendar API — events resource
https://developers.google.com/workspace/calendar/api/v3/reference/events -
Apple Calendar User Guide — repeating events
https://support.apple.com/guide/calendar/icl1018/mac
About the author
Elysiate publishes practical guides and privacy-first tools for data workflows, developer tooling, SEO, and product engineering.