diff --git a/search_api.admin.inc b/search_api.admin.inc
index 807db9f..1de6bfe 100644
--- a/search_api.admin.inc
+++ b/search_api.admin.inc
@@ -866,7 +866,7 @@ function search_api_admin_index_status_form(array $form, array &$form_state, Sea
       else {
         $form['index']['limit'] = array(
           '#type' => 'value',
-          '#value' => -1,
+          '#value' => 1,
         );
         $desc = t("This will immediately index all items that haven't been indexed in their latest version yet.");
       }
@@ -951,25 +951,22 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta
       $redirect = $pre . '/disable';
       break;
     case t('Index now'):
-      $limit = $values['limit'];
       $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
-      // @todo Batch API?
-      try {
-        $ret = search_api_index_items($index, ($limit < 0 || $batch_size < 0) ? -1 : $limit * $batch_size);
-      }
-      catch (SearchApiException $e) {
-        $ret = FALSE;
-        watchdog_exception('search_api', $e);
-      }
-      if ($ret) {
-        drupal_set_message(format_plural($ret, 'Successfully indexed 1 item.', 'Successfully indexed @count items.'));
-        if (($limit > 0 && $ret < min(array($limit, $values['remaining']))) || ($limit < 0 && $ret < $values['remaining'])) {
-          drupal_set_message(t("Some items couldn't be indexed. Check the logs for details."), 'warning');
-        }
-      }
-      else {
-        drupal_set_message(t("Couldn't index items. Check the logs for details."), 'error');
+      $limit = $values['limit'];
+      if ($limit < 0) {
+        // Add 1 to be a bit on the safe side.
+        $limit = (int) ceil($values['remaining'] / $batch_size) + 1;
       }
+      $batch = array(
+        'title' => t('Indexing items'),
+        'operations' => array(
+          array('_search_api_admin_index_now_callback', array($index, $batch_size, $limit)),
+        ),
+        'progress_message' => t('Completed indexing of @current batches of @total.'),
+        'finished' => '_search_api_admin_index_now_finished',
+        'file' => drupal_get_path('module', 'search_api') . '/search_api.admin.inc',
+      );
+      batch_set($batch);
       $redirect = $pre . '/status';
       break;
     case t('Re-index content'):
@@ -997,6 +994,68 @@ function search_api_admin_index_status_form_submit(array $form, array &$form_sta
 }
 
 /**
+ * Batch API callback for the "Index now" functionality.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be indexed.
+ * @param integer $batch_size
+ *   Number of items to index per batch.
+ * @param integer $limit
+ *   Number of batch runs.
+ * @param $context
+ *   The batch context.
+ */
+function _search_api_admin_index_now_callback(SearchApiIndex $index, $batch_size, $limit, array &$context) {
+  // Persistent data among batch runs.
+  if (!isset($context['sandbox']['index'])) {
+    $context['sandbox']['index'] = $index;
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['limit'] = $limit;
+    $context['sandbox']['batch_size'] = $batch_size;
+  }
+
+  // Persistent data to store results.
+  if (!isset($context['results']['indexed'])) {
+    $context['results']['indexed'] = 0;
+  }
+
+  if ($context['sandbox']['progress'] >= $context['sandbox']['limit']) {
+    $context['finished'] = 1;
+  }
+  else {
+    $context['sandbox']['progress']++;
+    $indexed = search_api_index_items($context['sandbox']['index'], $context['sandbox']['batch_size']);
+    $context['results']['indexed'] += $indexed;
+    $context['message'] = format_plural($context['results']['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['limit'];
+  }
+}
+
+/**
+ * Batch API finishing callback for the "Index now" functionality.
+ *
+ * @param boolean $success
+ *   Result of the batch operation.
+ * @param array $results
+ *   Results.
+ * @param array $operations
+ *   Remaining batch operation to process.
+ */
+function _search_api_admin_index_now_finished($success, $results, $operations) {
+  if ($success) {
+    if (!empty($results['indexed'])) {
+      drupal_set_message(format_plural($results['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.'));
+    }
+    else {
+      drupal_set_message(t("Couldn't index items. Check the logs for details."), 'error');
+    }
+  }
+  else {
+    drupal_set_message(t("An error occurred while trying to index items. Check the logs for details."), 'error');
+  }
+}
+
+/**
  * Edit an index' settings.
  *
  * @param SearchApiIndex $index
diff --git a/search_api.drush.inc b/search_api.drush.inc
index 9a02106..b0d79ab 100644
--- a/search_api.drush.inc
+++ b/search_api.drush.inc
@@ -43,11 +43,11 @@ function search_api_drush_command() {
       'drush sapi-i' => dt('Alias to index items for all enabled indexes.'),
       'drush sapi-i 1' => dt('Index items for the index with the ID !id.', array('!id' => 1)),
       'drush sapi-i default_node_index' => dt('Index items for the index with the machine name !name.', array('!name' => 'default_node_index')),
-      'drush sapi-i 1 100' => dt('Index a maximum number of !limit items for the index with the ID !id.', array('!limit' => 100, '!id' => 1)),
+      'drush sapi-i 1 100' => dt("Index a maximum number of !limit items * the index's cron batch size for the index with the ID !id.", array('!limit' => 100, '!id' => 1)),
     ),
     'arguments' => array(
       'index_id' => dt('The numeric ID or machine name of an index.'),
-      'limit' => dt("The number of items to index. Use 0 to index all items. Defaults to the index's cron batch size."),
+      'limit' => dt("The number of batch runs to index items (index's cron batch size per run). Use 0 to index all items. Defaults to 0 (index all)."),
     ),
     'aliases' => array('sapi-i'),
   );
@@ -170,18 +170,126 @@ function drush_search_api_index($index_id = NULL, $limit = NULL) {
   if (empty($indexes)) {
     return;
   }
-  $limit_string = $limit;
   foreach ($indexes as $index) {
-    if (is_null($limit)) {
-      $limit = $index->options['cron_limit'];
-      $limit_string = $limit;
+    $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
+    if (is_null($limit) || (int) $limit <= 0) {
+      $datasource = $index->datasource();
+      $index_status = $datasource->getIndexStatus($index);
+      $remaining = $index_status['total'] - $index_status['indexed'];
+
+      // Add 1 to be a bit on the safe side.
+      $limit = (int) ceil($remaining / $batch_size) + 1;
+      $limit_string = $remaining;
     }
-    elseif ((int) $limit <= 0) {
-      $limit = -1;
-      $limit_string = t('all');
+    else {
+      $limit_string = (int) $limit * $batch_size;
     }
     drush_print(dt('Indexing a maximum number of !limit items for index !index.', array('!index' => $index->name, '!limit' => $limit_string)));
-    search_api_index_items($index, $limit);
+
+    $batch = array(
+      'title' => t('Indexing items'),
+      'operations' => array(
+        array('_search_api_drush_batch_index_callback', array($index, $batch_size, $limit)),
+      ),
+      'progress_message' => dt('Completed indexing of @current batches of @total.'),
+      'finished' => '_search_api_drush_batch_index_finished',
+      'file' => drupal_get_path('module', 'search_api') . '/search_api.drush.inc',
+    );
+
+    batch_set($batch);
+    drush_backend_batch_process();
+  }
+}
+
+/**
+ * Batch API callback for the "Index now" functionality.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be indexed.
+ * @param integer $batch_size
+ *   Number of items to index per batch.
+ * @param integer $limit
+ *   Number of batch runs.
+ * @param $context
+ *   The batch context.
+ */
+function _search_api_drush_batch_index_callback(SearchApiIndex $index, $batch_size, $limit, array &$context) {
+  // Persistent data among batch runs.
+  if (!isset($context['sandbox']['index'])) {
+    $context['sandbox']['index'] = $index;
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['limit'] = $limit;
+    $context['sandbox']['batch_size'] = $batch_size;
+  }
+
+  // Persistent data to store results.
+  if (!isset($context['results']['indexed'])) {
+    $context['results']['indexed'] = 0;
+  }
+
+  if ($context['sandbox']['progress'] >= $context['sandbox']['limit']) {
+    $context['finished'] = 1;
+  }
+  else {
+    $context['sandbox']['progress']++;
+    $indexed = search_api_index_items($context['sandbox']['index'], $context['sandbox']['batch_size']);
+    $context['results']['indexed'] += $indexed;
+    $context['message'] = _search_api_drush_format_plural($context['results']['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['limit'];
+  }
+}
+
+/**
+ * Batch API finishing callback for the "Index now" functionality.
+ *
+ * @param boolean $success
+ *   Result of the batch operation.
+ * @param array $results
+ *   Results.
+ * @param array $operations
+ *   Remaining batch operation to process.
+ */
+function _search_api_drush_batch_index_finished($success, $results, $operations) {
+  if ($success) {
+    if (!empty($results['indexed'])) {
+      drush_log(_search_api_drush_format_plural($results['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.'), 'success');
+    }
+    else {
+      drush_log(dt("Couldn't index items. Check the logs for details."), 'error');
+    }
+  }
+  else {
+    drush_log(dt("An error occurred while trying to index items. Check the logs for details."), 'error');
+  }
+}
+
+/**
+ * Copy of formal_plural that works with drush as 't' may not be available.
+ */
+function _search_api_drush_format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
+  $args['@count'] = $count;
+  if ($count == 1) {
+    return dt($singular, $args, $options);
+  }
+
+  // Get the plural index through the gettext formula.
+  $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
+  // If the index cannot be computed, use the plural as a fallback (which
+  // allows for most flexiblity with the replaceable @count value).
+  if ($index < 0) {
+    return dt($plural, $args, $options);
+  }
+  else {
+    switch ($index) {
+      case "0":
+        return dt($singular, $args, $options);
+      case "1":
+        return dt($plural, $args, $options);
+      default:
+        unset($args['@count']);
+        $args['@count[' . $index . ']'] = $count;
+        return dt(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
+    }
   }
 }
 
