Google Calendar has a limitation where repeating events will stop repeating after they have occurred 730 times. Once an event has reached the limit of 730 occurrences it will stop appearing on your calendar without any warning, which can cause significant problems.
For an event that occurs daily this limit will be reached in just 2 years, and for an event that occurs weekly the limit will be reached in 14 years. That might seem like a long time but many people (including myself) have been using digital calendars for 20+ years, and as we continue to use them our risk increases for hitting this limit and having our events disappear.
This is a Google Apps Script that finds events that are close to reaching the limit of 730 occurrences as well as events that have already exceeded that limit. This script will tell you the title of each affected event so you can find it in your calendar and decide what to do with it. The script will also provide an estimate of when the event will reach the limit.
- Events that are not owned by you (for example, an event that someone else invited you to) will be skipped by this script.
- Depending on the size of your calendar and the limitations on your Google account it's possible that you might not have enough quota to run this script. If that happens you can try changing the
const daysToLookBackline to a smaller value. After running out of quota you may need to wait 24 hours before Google will let you run the script again. - This script uses estimates in several places - for example it assumes that months are always 30 days, and it will not properly count event exceptions (occurrences that were deleted or moved to other dates). Because of that the output from the script is an estimate, although that estimate should be good enough for most situations.
- I have almost no experience with JavaScript, so if something looks strange that's probably why. Feel free to suggest improvements in the comments.
- Go to https://script.google.com and create a New Project
- In the menu on the left, click the + button next to "Services" and add "Google Calendar API". If you see multiple versions of the API you should pick v3. See below:
- Copy/paste the below script into the code editor, modify the
const calendarIdvalue at the beginning to match your Google Calendar ID, and then save the script.- You can find your Calendar ID by going to Calendar settings, choosing your calendar from the "Settings for my calendars" menu on the left, and scrolling down until you see "Calendar ID." See below:

- Click the Run button and wait for the script to complete. This may take awhile so be patient - for reference my Google Calendar is a 6MB .ics file and the script takes about 5 minutes for me. See below:
function findRecurringEventsInDangerOfDisappearing() {
// --------------------------------------
// Change the below values as needed. calendarId MUST be set or this script will not work.
// --------------------------------------
const calendarId = "you@gmail.com"; // This is usually you@gmail.com.
const daysToLookBack = 1825; // Only look at events that have had at least one occurrence in the last 1825 days (five years). A larger value may find more events but will be slower and could run out of quota before the script completes. A smaller value will be faster but might miss some events.
const closenessThreshold = 0.5; // Output a list of events where at least 50% of their maximum occurrences have already happened.
// --------------------------------------
// Don't change anything below this line.
// --------------------------------------
if (calendarId == "you@gmail.com") {
throw new Error("calendarId must be set before you can run this script.");
}
const nowTime = new Date(new Date().toDateString()); // Today at midnight.
const startTime = new Date(nowTime.getFullYear(), nowTime.getMonth(), (nowTime.getDate() - daysToLookBack));
const gCalMaxOccurrences = 730; // From https://support.google.com/calendar/answer/37115
const oneDayInMilliseconds = 24 * 60 * 60 * 1000;
const numberOfOccurencesToAlertAt = (gCalMaxOccurrences * closenessThreshold);
var calendar = CalendarApp.getCalendarById(calendarId);
var events = calendar.getEvents(startTime, nowTime);
result = [];
events.reduce(function(idList, event) {
var eventId = event.getId();
// Skip events that we've already processed.
if (!idList.includes(eventId)) {
idList.push(eventId);
// Skip events that are not recurring and events that are owned by someone else.
if (event.isRecurringEvent() && event.isOwnedByMe()) {
var skipEvent = false;
var advancedId = eventId.replace(/@.*/, "");
var advancedEvent = Calendar.Events.get(calendarId, advancedId);
var eventFrequencyInDays = 0; // This value will be modified below
for (var i = 0; i < advancedEvent.recurrence.length; i++) {
var recurrence = advancedEvent.recurrence[i];
// An event can have multiple recurrence rules - only look at the ones containing RRULE (frequency data).
if (!recurrence.includes("RRULE")) {
continue;
}
// If the RRULE contains a COUNT that's less than or equal to 730 then this event will never reach the limit.
if (recurrence.includes("COUNT=")) {
if (recurrence.match(".*COUNT=(\\d+)")[1] <= gCalMaxOccurrences) {
skipEvent = true;
break;
}
}
// If the RRULE contains an UNTIL clause that's in the past then this event can be ignored.
if (recurrence.includes("UNTIL=")) {
var untilDate = recurrence.match(".*UNTIL=(\\d\\d\\d\\d)(\\d\\d)(\\d\\d)")
if (new Date(untilDate[1] + "-" + untilDate[2] + "-" + untilDate[3]) < nowTime) {
skipEvent = true;
break;
}
}
if (recurrence.includes("FREQ=DAILY")) {
eventFrequencyInDays = 1;
}
else if (recurrence.includes("FREQ=WEEKLY")) {
eventFrequencyInDays = 7;
}
// Assume a month is always 30 days for the purposes of this script.
else if (recurrence.includes("FREQ=MONTHLY")) {
eventFrequencyInDays = 30;
}
// Assume a year is always 365 days for the purposes of this script.
else if (recurrence.includes("FREQ=YEARLY")) {
eventFrequencyInDays = 365;
}
// If the RRULE contains an interval then adjust the frequency based on that.
if (recurrence.includes("INTERVAL=")) {
var interval = recurrence.match(".*INTERVAL=(\\d+)")[1];
eventFrequencyInDays = eventFrequencyInDays * interval;
}
// If we get here then we've already found the recurrence data, so there's no need to process any remaining RRULEs.
break;
}
if (eventFrequencyInDays == 0 && skipEvent == false) {
console.log("Unrecognized event frequency for '" + advancedEvent.summary + "': '" + recurrence + "'");
}
else if (skipEvent == false) {
var eventStartDate = new Date(advancedEvent.start['date']);
var daysSinceEventStarted = (Math.abs(eventStartDate - nowTime) / oneDayInMilliseconds);
// This count will not be exact, but it should be a close estimate.
var instancesOfEventSoFar = Math.round(daysSinceEventStarted / eventFrequencyInDays);
var estimatedExhaustionDate = new Date(eventStartDate.getFullYear(), eventStartDate.getMonth(), (eventStartDate.getDate() + (eventFrequencyInDays * gCalMaxOccurrences)));
if (instancesOfEventSoFar > numberOfOccurencesToAlertAt) {
result.push({eventSummary: advancedEvent.summary, eventStartDate: advancedEvent.start, eventExhaustionPercent: ((instancesOfEventSoFar / gCalMaxOccurrences) * 100).toPrecision(4) + "%", estimatedExhaustionDate: estimatedExhaustionDate});
}
}
}
}
return idList;
}, []);
console.log(result);
}


