diff --git a/resources/sql/autopatches/20161003.cal.01.utcepoch.sql b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql
new file mode 100644
--- /dev/null
+++ b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql
@@ -0,0 +1,8 @@
+ALTER TABLE {$NAMESPACE}_calendar.calendar_event
+  ADD utcInitialEpoch INT UNSIGNED NOT NULL;
+
+ALTER TABLE {$NAMESPACE}_calendar.calendar_event
+  ADD utcUntilEpoch INT UNSIGNED;
+
+ALTER TABLE {$NAMESPACE}_calendar.calendar_event
+  ADD utcInstanceEpoch INT UNSIGNED;
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
@@ -41,11 +41,16 @@
 
   protected $spacePHID;
 
+  protected $utcInitialEpoch;
+  protected $utcUntilEpoch;
+  protected $utcInstanceEpoch;
+
   private $parentEvent = self::ATTACHABLE;
   private $invitees = self::ATTACHABLE;
 
   private $viewerDateFrom;
   private $viewerDateTo;
+  private $viewerTimezone;
 
   // Frequency Constants
   const FREQUENCY_DAILY = 'daily';
@@ -298,6 +303,8 @@
         $zone);
     }
 
+    $this->viewerTimezone = $viewer->getTimezoneIdentifier();
+
     return $this;
   }
 
@@ -323,11 +330,62 @@
     return $dst->format('U');
   }
 
+  public function updateUTCEpochs() {
+    // The "intitial" epoch is the start time of the event, in UTC.
+    $start_date = $this->newStartDateTime()
+      ->setViewerTimezone('UTC');
+    $start_epoch = $start_date->getEpoch();
+    $this->setUTCInitialEpoch($start_epoch);
+
+    // The "until" epoch is the last UTC epoch on which any instance of this
+    // event occurs. For infinitely recurring events, it is `null`.
+
+    if (!$this->getIsRecurring()) {
+      $end_date = $this->newEndDateTime()
+        ->setViewerTimezone('UTC');
+      $until_epoch = $end_date->getEpoch();
+    } else {
+      $until_epoch = null;
+      $until_date = $this->newUntilDateTime()
+        ->setViewerTimezone('UTC');
+      if ($until_date) {
+        $duration = $this->newDuration();
+        $until_epoch = id(new PhutilCalendarRelativeDateTime())
+          ->setOrigin($until_date)
+          ->setDuration($duration)
+          ->getEpoch();
+      }
+    }
+    $this->setUTCUntilEpoch($until_epoch);
+
+    // The "instance" epoch is a property of instances of recurring events.
+    // It's the original UTC epoch on which the instance started. Usually that
+    // is the same as the start date, but they may be different if the instance
+    // has been edited.
+
+    // The ICS format uses this value (original start time) to identify event
+    // instances, and must do so because it allows additional arbitrary
+    // instances to be added (with "RDATE").
+
+    $instance_epoch = null;
+    $instance_date = $this->newInstanceDateTime();
+    if ($instance_date) {
+      $instance_epoch = $instance_date
+        ->setViewerTimezone('UTC')
+        ->getEpoch();
+    }
+    $this->setUTCInstanceEpoch($instance_epoch);
+
+    return $this;
+  }
+
   public function save() {
     if (!$this->mailKey) {
       $this->mailKey = Filesystem::readRandomCharacters(20);
     }
 
+    $this->updateUTCEpochs();
+
     return parent::save();
   }
 
@@ -363,6 +421,9 @@
         'instanceOfEventPHID' => 'phid?',
         'sequenceIndex' => 'uint32?',
         'isStub' => 'bool',
+        'utcInitialEpoch' => 'epoch',
+        'utcUntilEpoch' => 'epoch?',
+        'utcInstanceEpoch' => 'epoch?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'key_date' => array(
@@ -372,6 +433,13 @@
           'columns' => array('instanceOfEventPHID', 'sequenceIndex'),
           'unique' => true,
         ),
+        'key_epoch' => array(
+          'columns' => array('utcInitialEpoch', 'utcUntilEpoch'),
+        ),
+        'key_rdate' => array(
+          'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'),
+          'unique' => true,
+        ),
       ),
       self::CONFIG_SERIALIZATION => array(
         'recurrenceFrequency' => self::SERIALIZATION_JSON,
@@ -641,11 +709,8 @@
     $modified = $this->getDateModified();
     $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified);
 
-    $date_start = $this->getDateFrom();
-    $date_start = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_start);
-
-    $date_end = $this->getDateTo();
-    $date_end = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_end);
+    $date_start = $this->newStartDateTime();
+    $date_end = $this->newEndDateTime();
 
     if ($this->getIsAllDay()) {
       $date_start->setIsAllDay(true);
@@ -719,6 +784,57 @@
     return $node;
   }
 
+  public function newStartDateTime() {
+    $epoch = $this->getDateFrom();
+    return $this->newDateTimeFromEpoch($epoch);
+  }
+
+  public function newEndDateTime() {
+    $epoch = $this->getDateTo();
+    return $this->newDateTimeFromEpoch($epoch);
+  }
+
+  public function newUntilDateTime() {
+    $epoch = $this->getRecurrenceEndDate();
+    if (!$epoch) {
+      return null;
+    }
+    return $this->newDateTimeFromEpoch($epoch);
+  }
+
+  public function newDuration() {
+    return id(new PhutilCalendarDuration())
+      ->setSeconds($this->getDuration());
+  }
+
+  public function newInstanceDateTime() {
+    if (!$this->getIsRecurring()) {
+      return null;
+    }
+
+    $epochs = $this->getParent()->getSequenceIndexEpochs(
+      new PhabricatorUser(),
+      $this->getSequenceIndex(),
+      $this->getDuration());
+
+    $epoch = $epochs['dateFrom'];
+    return $this->newDateTimeFromEpoch($epoch);
+  }
+
+  private function newDateTimeFromEpoch($epoch) {
+    $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch);
+
+    $viewer_timezone = $this->viewerTimezone;
+    if ($viewer_timezone) {
+      $datetime->setViewerTimezone($viewer_timezone);
+    }
+
+    if ($this->getIsAllDay()) {
+      $datetime->setIsAllDay(true);
+    }
+
+    return $datetime;
+  }
 
 
 /* -(  Markup Interface  )--------------------------------------------------- */