diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
--- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
+++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
@@ -165,70 +165,54 @@
     // discard anything outside of the time window.
     $events = $this->getEventsInRange($events);
 
-    $enforced_end = null;
+    $generate_from = $this->rangeBegin;
+    $generate_until = $this->rangeEnd;
     foreach ($parents as $key => $event) {
-      $sequence_start = 0;
-      $sequence_end = null;
-      $start = null;
-
       $duration = $event->getDuration();
 
-      $frequency = $event->getFrequencyUnit();
-      $modify_key = '+1 '.$frequency;
-
-      if (($this->rangeBegin !== null) &&
-          ($this->rangeBegin > $event->getStartDateTimeEpoch())) {
-        $max_date = $this->rangeBegin - $duration;
-        $date = $event->getStartDateTimeEpoch();
-        $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
+      $start_date = $this->getRecurrenceWindowStart(
+        $event,
+        $generate_from - $duration);
 
-        while ($date < $max_date) {
-          // TODO: optimize this to not loop through all off-screen events
-          $sequence_start++;
-          $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
-          $date = $datetime->modify($modify_key)->format('U');
-        }
+      $end_date = $this->getRecurrenceWindowEnd(
+        $event,
+        $generate_until);
 
-        $start = $this->rangeBegin;
-      } else {
-        $start = $event->getStartDateTimeEpoch() - $duration;
-      }
+      $limit = $this->getRecurrenceLimit($event, $raw_limit);
 
-      $date = $start;
-      $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer);
+      $set = $event->newRecurrenceSet();
 
-      // Select the minimum end time we need to generate events until.
-      $end_times = array();
-      if ($this->rangeEnd) {
-        $end_times[] = $this->rangeEnd;
-      }
+      $recurrences = $set->getEventsBetween(
+        null,
+        $end_date,
+        $limit + 1);
 
-      if ($event->getUntilDateTimeEpoch()) {
-        $end_times[] = $event->getUntilDateTimeEpoch();
+      // We're generating events from the beginning and then filtering them
+      // here (instead of only generating events starting at the start date)
+      // because we need to know the proper sequence indexes to generate ghost
+      // events. This may change after RDATE support.
+      if ($start_date) {
+        $start_epoch = $start_date->getEpoch();
+      } else {
+        $start_epoch = null;
       }
 
-      if ($enforced_end) {
-        $end_times[] = $enforced_end;
-      }
+      foreach ($recurrences as $sequence_index => $sequence_datetime) {
+        if (!$sequence_index) {
+          // This is the parent event, which we already have.
+          continue;
+        }
 
-      if ($end_times) {
-        $end = min($end_times);
-        $sequence_end = $sequence_start;
-        while ($date < $end) {
-          $sequence_end++;
-          $datetime->modify($modify_key);
-          $date = $datetime->format('U');
-          if ($sequence_end > $raw_limit + $sequence_start) {
-            break;
+        if ($start_epoch) {
+          if ($sequence_datetime->getEpoch() < $start_epoch) {
+            continue;
           }
         }
-      } else {
-        $sequence_end = $raw_limit + $sequence_start;
-      }
 
-      $sequence_start = max(1, $sequence_start);
-      for ($index = $sequence_start; $index < $sequence_end; $index++) {
-        $events[] = $event->newGhost($viewer, $index);
+        $events[] = $event->newGhost(
+          $viewer,
+          $sequence_index,
+          $sequence_datetime);
       }
 
       // NOTE: We're slicing results every time because this makes it cheaper
@@ -240,7 +224,7 @@
         if (count($events) > $raw_limit) {
           $events = msort($events, 'getStartDateTimeEpoch');
           $events = array_slice($events, 0, $raw_limit, true);
-          $enforced_end = last($events)->getStartDateTimeEpoch();
+          $generate_until = last($events)->getEndDateTimeEpoch();
         }
       }
     }
@@ -525,4 +509,44 @@
     return $events;
   }
 
+  private function getRecurrenceWindowStart(
+    PhabricatorCalendarEvent $event,
+    $generate_from) {
+
+    if (!$generate_from) {
+      return null;
+    }
+
+    return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
+  }
+
+  private function getRecurrenceWindowEnd(
+    PhabricatorCalendarEvent $event,
+    $generate_until) {
+
+    $end_epochs = array();
+    if ($generate_until) {
+      $end_epochs[] = $generate_until;
+    }
+
+    $until_epoch = $event->getUntilDateTimeEpoch();
+    if ($until_epoch) {
+      $end_epochs[] = $until_epoch;
+    }
+
+    if (!$end_epochs) {
+      return null;
+    }
+
+    return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
+  }
+
+  private function getRecurrenceLimit(
+    PhabricatorCalendarEvent $event,
+    $raw_limit) {
+
+    return $raw_limit;
+  }
+
+
 }
diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
--- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php
+++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php
@@ -103,7 +103,10 @@
       ->applyViewerTimezone($actor);
   }
 
-  private function newChild(PhabricatorUser $actor, $sequence) {
+  private function newChild(
+    PhabricatorUser $actor,
+    $sequence,
+    PhutilCalendarDateTime $start = null) {
     if (!$this->isParentEvent()) {
       throw new Exception(
         pht(
@@ -124,7 +127,7 @@
       ->setDateFrom(0)
       ->setDateTo(0);
 
-    return $child->copyFromParent($actor);
+    return $child->copyFromParent($actor, $start);
   }
 
   protected function readField($field) {
@@ -156,7 +159,10 @@
   }
 
 
-  public function copyFromParent(PhabricatorUser $actor) {
+  public function copyFromParent(
+    PhabricatorUser $actor,
+    PhutilCalendarDateTime $start = null) {
+
     if (!$this->isChildEvent()) {
       throw new Exception(
         pht(
@@ -176,11 +182,18 @@
       ->setDescription($parent->getDescription());
 
     $sequence = $this->getSequenceIndex();
-    $start_datetime = $parent->newSequenceIndexDateTime($sequence);
 
-    if (!$start_datetime) {
-      throw new Exception(
-        "Sequence {$sequence} does not exist for event!");
+    if ($start) {
+      $start_datetime = $start;
+    } else {
+      $start_datetime = $parent->newSequenceIndexDateTime($sequence);
+
+      if (!$start_datetime) {
+        throw new Exception(
+          pht(
+            'Sequence "%s" is not valid for event!',
+            $sequence));
+      }
     }
 
     $duration = $parent->newDuration();
@@ -225,8 +238,12 @@
     return $stub;
   }
 
-  public function newGhost(PhabricatorUser $actor, $sequence) {
-    $ghost = $this->newChild($actor, $sequence);
+  public function newGhost(
+    PhabricatorUser $actor,
+    $sequence,
+    PhutilCalendarDateTime $start = null) {
+
+    $ghost = $this->newChild($actor, $sequence, $start);
 
     $ghost
       ->setIsGhostEvent(true)