diff --git a/README.txt b/README.txt
index 0ce279b..e058887 100644
--- a/README.txt
+++ b/README.txt
@@ -48,6 +48,19 @@ Install as you would normally install a contributed Drupal module. See
 https://drupal.org/documentation/install/modules-themes/modules-7
 for further information.
 
+SCALABILITY
+-----------
+The password expiration rule is checked on cron. If you have a large number
+of users, this check can consume a lot of time and system resources. It's
+possible to disable running this check in cron by setting a variable
+password_policy_process_on_cron to FALSE, e.g. in settings.php:
+
+  $conf['password_policy_process_on_cron'] = FALSE;
+
+Then you should manually run the policy at a time when system resources are
+more likely to be available (e.g. at night) with a command like:
+
+  drush ev "password_policy_process_expirations();"
 
 LIMITATIONS
 -----------
diff --git a/password_policy.module b/password_policy.module
index e807682..38dd02d 100644
--- a/password_policy.module
+++ b/password_policy.module
@@ -542,6 +542,22 @@ function password_policy_form_alter(&$form, &$form_state, $form_id) {
  * Implements hook_cron().
  */
 function password_policy_cron() {
+  if (variable_get('password_policy_process_on_cron', TRUE)) {
+    password_policy_process_expirations();
+  }
+  else {
+    watchdog('password_policy', 'Skipping password policy during cron per variable configuration.', array(), WATCHDOG_DEBUG);
+  }
+}
+
+/**
+ * Actually processes the expiration rule.
+ *
+ * @see password_policy_cron()
+ *
+ * @throws \Exception
+ */
+function password_policy_process_expirations() {
   // Short circuit if no policies are active that use expiration.
   $expiration_policies = db_select('password_policy', 'p', array('target' => 'slave'))
     ->condition('enabled', 1)
@@ -553,21 +569,41 @@ function password_policy_cron() {
     return;
   }
 
+  // Find affected roles. If authenticated is not affected by an expiration then
+  // do a faster query that involves more joins, more indexes, fewer rows.
+  $count_query = db_select('password_policy', 'p', array('target' => 'slave'));
+  $count_query->join('password_policy_role', 'r', 'p.pid = r.pid');
+  $expiration_policies_for_auth = $count_query
+    ->condition('p.enabled', 1)
+    ->condition('p.expiration', 0, '>')
+    ->condition('r.rid', 2)
+    ->countQuery()
+    ->execute()
+    ->fetchField();
+
   $accounts = array();
   $warns = array();
   $unblocks = array();
   $pids = array();
 
-  // Get all users' last password change time. We don't touch blocked accounts.
+  // Get all users' last password change time. Don't touch blocked accounts.
   $query = db_select('users', 'u', array('target' => 'slave'));
-  $query->leftJoin('password_policy_history', 'p', 'u.uid = p.uid');
+  $query->leftJoin('password_policy_history', 'h', 'u.uid = h.uid');
   $query->leftJoin('password_policy_expiration', 'e', 'u.uid = e.uid');
+
+  if ($expiration_policies_for_auth == 0) {
+    $query->join('users_roles', 'ur', 'u.uid = ur.uid');
+    $query->join('password_policy_role', 'r', 'ur.rid = r.rid');
+    $query->join('password_policy', 'p', 'r.pid = p.pid');
+    $query->condition('p.enabled', 1)
+      ->condition('p.expiration', 0, '>');
+  }
+
   $result = $query->fields('u', array('uid', 'created'))
-    ->fields('p', array('created'))
+    ->fields('h', array('created'))
     ->fields('e', array('pid', 'unblocked', 'warning'))
     ->condition('u.uid', 0, '>')
     ->condition('u.status', 1)
-    ->orderBy('p.created')
     ->execute();
 
   foreach ($result as $row) {
