diff --git a/search_api.install b/search_api.install
index 2777f88..ae4eb4b 100644
--- a/search_api.install
+++ b/search_api.install
@@ -5,6 +5,8 @@
  * Install, update and uninstall functions for the Search API module.
  */
 
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Link;
 use Drupal\search_api\Entity\Server;
 use Drupal\Core\Url;
 
@@ -121,6 +123,7 @@ function search_api_requirements($phase) {
       );
     }
 
+    /** @var \Drupal\search_api\ServerInterface[] $servers */
     $servers = Server::loadMultiple();
     $unavailable_servers = array();
     foreach ($servers as $server) {
@@ -143,6 +146,31 @@ function search_api_requirements($phase) {
       );
     }
 
+    $pending_tasks = \Drupal::getContainer()
+      ->get('search_api.server_task_manager')
+      ->getCount();
+    if ($pending_tasks) {
+      $args['@link'] = '';
+      $url = Url::fromRoute('entity.search_api_server.execute_tasks');
+      if ($url->access()) {
+        $link = new Link(t('Execute now'), $url);
+        $link = $link->toString();
+        $args['@link'] = $link;
+        $args['@link'] = new FormattableMarkup(' (@link)', $args);
+      }
+
+      $requirements['search_api_pending_tasks'] = array(
+        'title' => t('Search API'),
+        'value' => \Drupal::translation()->formatPlural(
+          $pending_tasks,
+          'There is @count pending task for a Search API server. @link',
+          'There are @count pending tasks for Search API servers. @link',
+          $args
+        ),
+        'severity' => REQUIREMENT_WARNING,
+      );
+    }
+
     return $requirements;
   }
   return array();
diff --git a/search_api.links.action.yml b/search_api.links.action.yml
index f1a18f2..11096f9 100644
--- a/search_api.links.action.yml
+++ b/search_api.links.action.yml
@@ -8,6 +8,11 @@ entity.search_api_index.add_form:
   title: 'Add index'
   appears_on:
     - search_api.overview
+entity.search_api_server.execute_tasks:
+  route_name: entity.search_api_server.execute_tasks
+  title: 'Execute pending tasks'
+  appears_on:
+    - search_api.overview
 entity.search_api_index.add_fields:
   route_name: entity.search_api_index.add_fields
   title: 'Add fields'
diff --git a/search_api.routing.yml b/search_api.routing.yml
index 1b1cdf3..21b6ae4 100644
--- a/search_api.routing.yml
+++ b/search_api.routing.yml
@@ -6,6 +6,15 @@ search_api.overview:
   requirements:
     _permission: 'administer search_api'
 
+entity.search_api_server.execute_tasks:
+  path: '/admin/config/search/search-api/execute-tasks'
+  defaults:
+    _controller: '\Drupal\search_api\Controller\ServerController::executeTasks'
+    _title: 'Execute pending server tasks'
+  requirements:
+    _permission: 'administer search_api'
+    _search_api_tasks: 'TRUE'
+
 entity.search_api_server.add_form:
   path: '/admin/config/search/search-api/add-server'
   defaults:
diff --git a/search_api.services.yml b/search_api.services.yml
index 61db9d4..8f9f8b2 100644
--- a/search_api.services.yml
+++ b/search_api.services.yml
@@ -1,4 +1,10 @@
 services:
+  access_check.search_api_tasks:
+    class: Drupal\search_api\Controller\ExecuteTasksAccessCheck
+    arguments: ['@search_api.server_task_manager']
+    tags:
+      - { name: access_check, applies_to: _search_api_tasks }
+
   paramconverter.search_api:
     class: Drupal\search_api\ParamConverter\SearchApiConverter
     arguments: ['@entity.manager', '@user.shared_tempstore', '@current_user']
diff --git a/src/Controller/ExecuteTasksAccessCheck.php b/src/Controller/ExecuteTasksAccessCheck.php
new file mode 100644
index 0000000..ceb9ce4
--- /dev/null
+++ b/src/Controller/ExecuteTasksAccessCheck.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\search_api\Controller;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\search_api\Task\ServerTaskManagerInterface;
+
+/**
+ * Provides an access check for the "Execute server tasks" route.
+ */
+class ExecuteTasksAccessCheck implements AccessInterface {
+
+  /**
+   * The server tasks manager service.
+   *
+   * @var \Drupal\search_api\Task\ServerTaskManagerInterface
+   */
+  protected $serverTasksManager;
+
+  /**
+   * Creates an ExecuteTasksAccessCheck object.
+   *
+   * @param \Drupal\search_api\Task\ServerTaskManagerInterface $serverTasksManager
+   */
+  public function __construct(ServerTaskManagerInterface $serverTasksManager) {
+    $this->serverTasksManager = $serverTasksManager;
+  }
+
+  /**
+   * Checks access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access() {
+    if ($this->serverTasksManager->getCount()) {
+      return AccessResult::allowed()->setCacheMaxAge(0);
+    }
+    return AccessResult::forbidden()->setCacheMaxAge(0);
+  }
+
+}
diff --git a/src/Controller/ServerController.php b/src/Controller/ServerController.php
index 60510c3..91df512 100644
--- a/src/Controller/ServerController.php
+++ b/src/Controller/ServerController.php
@@ -4,7 +4,10 @@
 
 use Drupal\Component\Render\FormattableMarkup;
 use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
 use Drupal\search_api\ServerInterface;
+use Drupal\search_api\Task\ServerTaskManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides block routines for search server-specific routes.
@@ -12,6 +15,47 @@
 class ServerController extends ControllerBase {
 
   /**
+   * The server task manager.
+   *
+   * @var \Drupal\search_api\Task\ServerTaskManagerInterface|null
+   */
+  protected $serverTaskManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var static $controller */
+    $controller = parent::create($container);
+
+    $controller->setServerTaskManager($container->get('search_api.server_task_manager'));
+
+    return $controller;
+  }
+
+  /**
+   * Retrieves the server task manager.
+   *
+   * @return \Drupal\search_api\Task\ServerTaskManagerInterface
+   *   The server task manager.
+   */
+  public function getServerTaskManager() {
+    return $this->serverTaskManager ?: \Drupal::service('search_api.server_task_manager');
+  }
+
+  /**
+   * Sets the server task manager.
+   *
+   * @param \Drupal\search_api\Task\ServerTaskManagerInterface $server_task_manager
+   *   The new server task manager.
+   *
+   * @return $this
+   */
+  public function setServerTaskManager(ServerTaskManagerInterface $server_task_manager) {
+    $this->serverTaskManager = $server_task_manager;
+    return $this;
+  }
+  /**
    * Displays information about a search server.
    *
    * @param \Drupal\search_api\ServerInterface $search_api_server
@@ -72,4 +116,12 @@ public function serverBypassEnable(ServerInterface $search_api_server) {
     return $this->redirect($url->getRouteName(), $url->getRouteParameters());
   }
 
+  /**
+   * Executes all pending tasks of any (enabled) servers.
+   */
+  public function executeTasks() {
+    $this->getServerTaskManager()->setExecuteBatch();
+    return batch_process(Url::fromRoute('search_api.overview'));
+  }
+
 }
diff --git a/src/Entity/Server.php b/src/Entity/Server.php
index 317f3c3..6bcb619 100644
--- a/src/Entity/Server.php
+++ b/src/Entity/Server.php
@@ -210,7 +210,10 @@ public function addIndex(IndexInterface $index) {
     $server_task_manager->delete(NULL, $this, $index);
 
     try {
-      $this->getBackend()->addIndex($index);
+      if ($server_task_manager->execute($this)) {
+        $this->getBackend()->addIndex($index);
+        return;
+      }
     }
     catch (SearchApiException $e) {
       $vars = array(
@@ -253,7 +256,10 @@ public function removeIndex($index) {
     $server_task_manager->delete(NULL, $this, $index);
 
     try {
-      $this->getBackend()->removeIndex($index);
+      if ($server_task_manager->execute($this)) {
+        $this->getBackend()->removeIndex($index);
+        return;
+      }
     }
     catch (SearchApiException $e) {
       $vars = array(
@@ -318,6 +324,15 @@ public function deleteAllIndexItems(IndexInterface $index) {
     }
 
     $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+
+    // Remove all other delete operations for this index from the server tasks –
+    // no point in executing those when we want to delete all items anyways.
+    $types = array(
+      'deleteItems',
+      'deleteAllIndexItems',
+    );
+    $server_task_manager->delete(NULL, $this, $index, $types);
+
     try {
       if ($server_task_manager->execute($this)) {
         $this->getBackend()->deleteAllIndexItems($index);
@@ -361,7 +376,14 @@ public function deleteAllItems() {
       $message = new FormattableMarkup('Deleting all items from server %server failed for the following (write-enabled) indexes: @indexes.', $args);
       throw new SearchApiException($message, 0, $e);
     }
-    return $this;
+
+    $types = array(
+      'deleteItems',
+      'deleteAllIndexItems',
+    );
+    \Drupal::getContainer()
+      ->get('search_api.server_task_manager')
+      ->delete(NULL, $this, NULL, $types);
   }
 
   /**
diff --git a/src/Form/ServerStatusForm.php b/src/Form/ServerStatusForm.php
index 0dbdf2e..3693180 100644
--- a/src/Form/ServerStatusForm.php
+++ b/src/Form/ServerStatusForm.php
@@ -5,6 +5,8 @@
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\search_api\ServerInterface;
+use Drupal\search_api\Task\ServerTaskManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Provides a form for performing common actions on a server.
@@ -12,6 +14,47 @@
 class ServerStatusForm extends FormBase {
 
   /**
+   * The server task manager.
+   *
+   * @var \Drupal\search_api\Task\ServerTaskManagerInterface|null
+   */
+  protected $serverTaskManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var static $form */
+    $form = parent::create($container);
+
+    $form->setServerTaskManager($container->get('search_api.server_task_manager'));
+
+    return $form;
+  }
+
+  /**
+   * Retrieves the server task manager.
+   *
+   * @return \Drupal\search_api\Task\ServerTaskManagerInterface
+   *   The server task manager.
+   */
+  public function getServerTaskManager() {
+    return $this->serverTaskManager ?: \Drupal::service('search_api.server_task_manager');
+  }
+
+  /**
+   * Sets the server task manager.
+   *
+   * @param \Drupal\search_api\Task\ServerTaskManagerInterface $server_task_manager
+   *   The new server task manager.
+   *
+   * @return $this
+   */
+  public function setServerTaskManager(ServerTaskManagerInterface $server_task_manager) {
+    $this->serverTaskManager = $server_task_manager;
+    return $this;
+  }
+  /**
    * {@inheritdoc}
    */
   public function getFormId() {
@@ -24,6 +67,30 @@ public function getFormId() {
   public function buildForm(array $form, FormStateInterface $form_state, ServerInterface $server = NULL) {
     $form['#server'] = $server;
 
+    $pending_tasks = $this->getServerTaskManager()->getCount($server);
+    if ($pending_tasks) {
+      $status = $this->formatPlural(
+        $pending_tasks,
+        'There is currently @count task pending for this server.',
+        'There are currently @count tasks pending for this server.'
+      );
+      $form['tasks'] = array(
+        '#type' => 'fieldset',
+        '#title' => $this->t('Pending server tasks'),
+      );
+      $form['tasks']['help'] = array(
+        '#type' => 'item',
+        '#title' => $status,
+        '#description' => $this->t('Pending tasks are created when operations on the server, such as deleting one or more items, cannot be executed because the server is currently unavailable (which will usually also create an entry in the Drupal logs). They are automatically tried again before any other operation is executed and the operation is aborted if the tasks could still not be executed, or if there are too many pending tasks to be executed in a single page request. In the latter case, you can use this form to manually execute all tasks and thus unblock the server again.'),
+      );
+      $form['tasks']['execute'] = array(
+        '#type' => 'submit',
+        '#value' => $this->t('Execute tasks now'),
+        '#disabled' => !$server->isAvailable(),
+        '#submit' => array('::executeTasks'),
+      );
+    }
+
     $form['actions']['#type'] = 'actions';
     $form['actions']['clear'] = array(
       '#type' => 'submit',
@@ -36,6 +103,15 @@ public function buildForm(array $form, FormStateInterface $form_state, ServerInt
   /**
    * {@inheritdoc}
    */
+  public function executeTasks(array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\search_api\ServerInterface $server */
+    $server = $form['#server'];
+    $this->getServerTaskManager()->setExecuteBatch($server);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     // Redirect to the "Clear server" confirmation form.
     /** @var \Drupal\search_api\ServerInterface $server */
diff --git a/src/ServerInterface.php b/src/ServerInterface.php
index d4569b9..3215bec 100644
--- a/src/ServerInterface.php
+++ b/src/ServerInterface.php
@@ -77,8 +77,6 @@ public function getIndexes(array $properties = array());
   /**
    * Deletes all items on this server, except those from read-only indexes.
    *
-   * @return $this
-   *
    * @throws \Drupal\search_api\SearchApiException
    *   Thrown if an error occurred while trying to delete the items.
    */
diff --git a/src/Task/ServerTaskManager.php b/src/Task/ServerTaskManager.php
index 0ad1e4e..02558e2 100644
--- a/src/Task/ServerTaskManager.php
+++ b/src/Task/ServerTaskManager.php
@@ -3,7 +3,10 @@
 namespace Drupal\search_api\Task;
 
 use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\DependencyInjection\DependencySerializationTrait;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\search_api\SearchApiException;
 use Drupal\search_api\IndexInterface;
 use Drupal\search_api\ServerInterface;
@@ -13,6 +16,9 @@
  */
 class ServerTaskManager implements ServerTaskManagerInterface {
 
+  use StringTranslationTrait;
+  use DependencySerializationTrait;
+
   /**
    * The database connection.
    *
@@ -43,36 +49,37 @@ public function __construct(Connection $database, EntityTypeManagerInterface $en
   /**
    * {@inheritdoc}
    */
+  public function getCount(ServerInterface $server = NULL) {
+    return $this->createQuery($server)
+      ->range()
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function execute(ServerInterface $server = NULL) {
-    $select = $this->database->select('search_api_task', 't');
-    $tasks = array(
-      'addIndex',
-      'updateIndex',
-      'removeIndex',
-      'deleteItems',
-      'deleteAllIndexItems',
-    );
-    $select->fields('t')
-      // Only retrieve tasks we can handle.
-      ->condition('t.type', $tasks, 'IN');
-    if ($server) {
-      if (!$server->status()) {
-        return FALSE;
-      }
-      $select->condition('t.server_id', $server->id());
-    }
-    else {
-      // By ordering by the server, we can later just load them when we reach
-      // them while looping through the tasks. It is very unlikely there will be
-      // tasks for more than one or two servers, so a *_load_multiple() probably
-      // wouldn't bring any significant advantages, but complicate the code.
-      $select->orderBy('t.server_id');
+    if ($server && !$server->status()) {
+      return FALSE;
     }
 
-    // Sometimes the order of tasks might be important, so make sure to order by
-    // the task ID (which should be in order of insertion).
-    $select->orderBy('t.id');
+    $select = $this->createQuery($server);
+
+    // Execute the SELECT query and remember the limit used. (Weirdly, the
+    // query object doesn't seem to have a method for retrieving the range after
+    // setting it.)
     $tasks = $select->execute();
+    $fetch_count = $select->getMetaData('search_api_server_tasks_limit');
+
+    // After all possible modifications to the query have been made, we can now
+    // also determine the total task count we would have had. Unfortunately,
+    // since we had to set the range before (which countQuery() doesn't
+    // automatically remove for some reason), this isn't completely
+    // straight-forward.
+    $count_query = clone $select;
+    $total_count = $count_query->range()->countQuery()->execute()->fetchField();
 
     $executed_tasks = array();
     $failing_servers = array();
@@ -95,47 +102,9 @@ public function execute(ServerInterface $server = NULL) {
         $index = $this->loadIndex($task->index_id);
       }
       try {
-        switch ($task->type) {
-          case 'addIndex':
-            if ($index) {
-              $server->getBackend()->addIndex($index);
-            }
-            break;
-
-          case 'updateIndex':
-            if ($index) {
-              if ($task->data) {
-                $index->original = unserialize($task->data);
-              }
-              $server->getBackend()->updateIndex($index);
-            }
-            break;
-
-          case 'removeIndex':
-            if ($index) {
-              $server->getBackend()->removeIndex($index ? $index : $task->index_id);
-              $this->delete(NULL, $server, $index);
-            }
-            break;
-
-          case 'deleteItems':
-            if ($index && !$index->isReadOnly()) {
-              $ids = unserialize($task->data);
-              $server->getBackend()->deleteItems($index, $ids);
-            }
-            break;
-
-          case 'deleteAllIndexItems':
-            if ($index && !$index->isReadOnly()) {
-              $server->getBackend()->deleteAllIndexItems($index);
-            }
-            break;
-
-          default:
-            // This should never happen.
-            continue 2;
+        if ($this->executeTask($task, $server, $index)) {
+          $executed_tasks[] = $task->id;
         }
-        $executed_tasks[] = $task->id;
       }
       catch (SearchApiException $e) {
         // If a task fails, we don't want to execute any other tasks for that
@@ -150,8 +119,221 @@ public function execute(ServerInterface $server = NULL) {
       $this->delete($executed_tasks);
     }
     // Return TRUE if no tasks failed (i.e., if we didn't mark any server as
-    // failing).
-    return !$failing_servers;
+    // failing) and we retrieved all of them.
+    return !$failing_servers && $total_count <= $fetch_count;
+  }
+
+  /**
+   * Creates a SELECT query for retrieving tasks.
+   *
+   * @param \Drupal\search_api\ServerInterface|null $server
+   *   (optional) The server for which to retrieve tasks, or NULL to retrieve
+   *   tasks for all servers.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A SELECT query for retrieving the tasks.
+   */
+  protected function createQuery(ServerInterface $server = NULL) {
+    $select = $this->database->select('search_api_task', 't');
+    $select->fields('t');
+    // Only retrieve tasks we can handle.
+    $select->condition('t.type', $this->getSupportedTypes(), 'IN');
+    if ($server) {
+      $select->condition('t.server_id', $server->id());
+    }
+    else {
+      // By ordering by the server, we can later just load them when we reach
+      // them while looping through the tasks. It is very unlikely there will be
+      // tasks for more than one or two servers, so a *_load_multiple() probably
+      // wouldn't bring any significant advantages, but complicate the code.
+      $select->orderBy('t.server_id');
+    }
+
+    // Sometimes the order of tasks might be important, so make sure to order by
+    // the task ID (which should be in order of insertion).
+    $select->orderBy('t.id');
+
+    // Limit number of tasks executed in one call of this method, to avoid
+    // breaking the site completely if there are too many tasks.
+    $select->range(0, 100);
+
+    // Add a tag and metadata in case other modules want to modify this query.
+    // The "search_api_server_tasks_limit" metadata is provided for modules that
+    // change the limit to update it to the new one, so we know about it.
+    $select->addTag('search_api_server_tasks')
+      ->addMetaData('search_api_server', $server)
+      ->addMetaData('search_api_server_tasks_limit', 100);
+
+    // Make it easier for a subclass to change the query.
+    $this->preprocessQuery($select, $server);
+    return $select;
+  }
+
+  /**
+   * Retrieves the task types supported by this task manager.
+   *
+   * @return string[]
+   *   The task types supported by this task manager.
+   */
+  protected function getSupportedTypes() {
+    return array(
+      'addIndex',
+      'updateIndex',
+      'removeIndex',
+      'deleteItems',
+      'deleteAllIndexItems',
+    );
+  }
+
+  /**
+   * Preprocesses the query used to retrieve tasks.
+   *
+   * Does nothing by default, but can be overridden by subclasses.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $select
+   *   The constructed SELECT query.
+   * @param \Drupal\search_api\ServerInterface|null $server
+   *   (optional) The server for which tasks are retrieved, or NULL if tasks
+   *   for all servers are retrieved.
+   */
+  protected function preprocessQuery(SelectInterface $select, ServerInterface $server = NULL) {}
+
+  /**
+   * Executes a single server task.
+   *
+   * @param object $task
+   *   A task object, as retrieved from the search_api_task table.
+   * @param \Drupal\search_api\ServerInterface $server
+   *   The server on which to execute the task.
+   * @param \Drupal\search_api\IndexInterface|null $index
+   *   (optional) The index to which the task pertains, if any.
+   *
+   * @return bool
+   *   TRUE if the task was successfully executed, FALSE if the task type was
+   *   unknown.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   If any error occurred while executing the task.
+   */
+  protected function executeTask($task, ServerInterface $server, IndexInterface $index = NULL) {
+    switch ($task->type) {
+      case 'addIndex':
+        if ($index) {
+          $server->getBackend()->addIndex($index);
+        }
+        return TRUE;
+
+      case 'updateIndex':
+        if ($index) {
+          if ($task->data) {
+            $index->original = unserialize($task->data);
+          }
+          $server->getBackend()->updateIndex($index);
+        }
+        return TRUE;
+
+      case 'removeIndex':
+        if ($index) {
+          $server->getBackend()->removeIndex($index ? $index : $task->index_id);
+          $this->delete(NULL, $server, $index);
+        }
+        return TRUE;
+
+      case 'deleteItems':
+        if ($index && !$index->isReadOnly()) {
+          $ids = unserialize($task->data);
+          $server->getBackend()->deleteItems($index, $ids);
+        }
+        return TRUE;
+
+      case 'deleteAllIndexItems':
+        if ($index && !$index->isReadOnly()) {
+          $server->getBackend()->deleteAllIndexItems($index);
+        }
+        return TRUE;
+    }
+
+    // We didn't know that type of task.
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setExecuteBatch(ServerInterface $server = NULL) {
+    $batch_definition = array(
+      'operations' => array(
+        array(array($this, 'processBatch'), array($server ? $server->id() : NULL)),
+      ),
+      'finished' => array($this, 'finishBatch'),
+    );
+    // Schedule the batch.
+    batch_set($batch_definition);
+  }
+
+  /**
+   * Executes server tasks as part of a batch operation.
+   *
+   * @param string|null $server_id
+   *   The ID of the server for which tasks should be executed, or NULL to
+   *   execute tasks on all servers.
+   * @param array $context
+   *   The current batch context, as defined in the @link batch Batch operations
+   *   @endlink documentation.
+   */
+  public function processBatch($server_id, array &$context) {
+    /** @var \Drupal\search_api\IndexInterface $server */
+    $server = $server_id ? $this->loadServer($server_id) : $server_id;
+
+    if (!isset($context['results']['total'])) {
+      $context['results']['executed'] = 0;
+      $context['results']['total'] = $this->getCount($server);
+      if (!$context['results']['total']) {
+        $context['finished'] = 1;
+        return;
+      }
+    }
+
+    $finished = $this->execute($server);
+    $still_pending = $this->getCount($server);
+    $executed = $context['results']['total'] - $still_pending;
+    if ($finished  || $executed == $context['results']['executed']) {
+      $context['finished'] = 1;
+    }
+    else {
+      $context['finished'] = $still_pending / $context['results']['total'];
+      $context['message'] = $this->formatPlural($executed, 'Executed @count task.', 'Executed @count tasks.');
+    }
+    $context['results']['executed'] = $executed;
+  }
+
+  /**
+   * Finishes an "execute server tasks" batch.
+   *
+   * @param bool $success
+   *   Indicates whether the batch process was successful.
+   * @param array $results
+   *   Results information passed from the processing callback.
+   */
+  public function finishBatch($success, $results) {
+    // Check if the batch job was successful.
+    if ($success) {
+      if ($results['executed']) {
+        // Display the number of tasks executed.
+        $message = $this->formatPlural($results['executed'], 'Successfully executed @count task.', 'Successfully executed @count tasks.');
+        drupal_set_message($message);
+      }
+      if (!$results['total']) {
+        drupal_set_message($this->t('There were no tasks to execute.'), 'warning');
+      }
+      elseif ($results['executed'] < $results['total']) {
+        drupal_set_message($this->t('An error occurred while trying to execute the tasks. Check the logs for details.'), 'error');
+      }
+    }
+    else {
+      // Notify the user about the batch job failure.
+      drupal_set_message($this->t('An error occurred while trying to execute the tasks. Check the logs for details.'), 'error');
+    }
   }
 
   /**
@@ -171,7 +353,7 @@ public function add(ServerInterface $server, $type, IndexInterface $index = NULL
   /**
    * {@inheritdoc}
    */
-  public function delete(array $ids = NULL, ServerInterface $server = NULL, $index = NULL) {
+  public function delete(array $ids = NULL, ServerInterface $server = NULL, $index = NULL, array $types = NULL) {
     $delete = $this->database->delete('search_api_task');
     if ($ids) {
       $delete->condition('id', $ids, 'IN');
@@ -182,6 +364,9 @@ public function delete(array $ids = NULL, ServerInterface $server = NULL, $index
     if ($index) {
       $delete->condition('index_id', $index instanceof IndexInterface ? $index->id() : $index);
     }
+    if ($types) {
+      $delete->condition('type', $types, 'IN');
+    }
     $delete->execute();
   }
 
diff --git a/src/Task/ServerTaskManagerInterface.php b/src/Task/ServerTaskManagerInterface.php
index 30a8da3..c6adc39 100644
--- a/src/Task/ServerTaskManagerInterface.php
+++ b/src/Task/ServerTaskManagerInterface.php
@@ -11,11 +11,23 @@
 interface ServerTaskManagerInterface {
 
   /**
+   * Retrieves the number of pending server tasks.
+   *
+   * @param \Drupal\search_api\ServerInterface|null $server
+   *   (optional) The server whose tasks should be counted, or NULL to count all
+   *   tasks.
+   *
+   * @return int
+   *   The number of tasks pending for this server, or in total.
+   */
+  public function getCount(ServerInterface $server = NULL);
+
+  /**
    * Checks for pending tasks on one or all enabled search servers.
    *
    * @param \Drupal\search_api\ServerInterface|null $server
-   *   (optional) The server whose tasks should be checked. If not given, the
-   *   tasks for all enabled servers are checked.
+   *   (optional) The server whose tasks should be executed. If not given, the
+   *   tasks for all enabled servers are executed.
    *
    * @return bool
    *   TRUE if all tasks (for the specific server, if $server was given) were
@@ -25,6 +37,15 @@
   public function execute(ServerInterface $server = NULL);
 
   /**
+   * Sets a batch for executing server tasks.
+   *
+   * @param \Drupal\search_api\ServerInterface|null $server
+   *   (optional) The server whose tasks should be executed. If not given, the
+   *   tasks for all enabled servers are executed.
+   */
+  public function setExecuteBatch(ServerInterface $server = NULL);
+
+  /**
    * Adds an entry into a server's list of pending tasks.
    *
    * @param \Drupal\search_api\ServerInterface $server
@@ -51,7 +72,10 @@ public function add(ServerInterface $server, $type, IndexInterface $index = NULL
    * @param \Drupal\search_api\IndexInterface|string|null $index
    *   (optional) An index (or its ID) for which the tasks should be deleted.
    *   Set to NULL to delete tasks for all indexes.
+   * @param string[]|null $types
+   *   (optional) The types of tasks that should be deleted, or NULL to delete
+   *   tasks regardless of type.
    */
-  public function delete(array $ids = NULL, ServerInterface $server = NULL, $index = NULL);
+  public function delete(array $ids = NULL, ServerInterface $server = NULL, $index = NULL, array $types = NULL);
 
 }
diff --git a/src/Tests/OverviewPageTest.php b/src/Tests/OverviewPageTest.php
index d250995..dc8a9df 100644
--- a/src/Tests/OverviewPageTest.php
+++ b/src/Tests/OverviewPageTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\search_api\Tests;
 
+use Drupal\block\Entity\Block;
 use Drupal\Component\Utility\Html;
 use Drupal\search_api\ServerInterface;
 
@@ -57,6 +58,19 @@ public function testServerAndIndexCreation() {
     $server_name = 'WebTest Server';
     $index_name = 'WebTest Index';
 
+    $success = $this->container->get('module_installer')->install(array('block'), TRUE);
+    $this->assertTrue($success, 'Block module enabled.');
+
+    // Enable the "Local actions" block so we can verify which local actions are
+    // displayed.
+    Block::create(array(
+      'id' => 'classy_local_actions',
+      'theme' => 'classy',
+      'weight' => -20,
+      'plugin' => 'local_actions_block',
+      'region' => 'content',
+    ))->save();
+
     // Make sure the overview is empty.
     $this->drupalGet($this->overviewPageUrl);
 
@@ -80,6 +94,19 @@ public function testServerAndIndexCreation() {
     $this->assertText($index->label(), 'Index present on overview page.');
     $this->assertRaw($index->get('description'), 'Index description is present');
     $this->assertFieldByXPath('//tr[contains(@class,"' . Html::cleanCssIdentifier($index->getEntityTypeId() . '-' . $index->id()) . '") and contains(@class, "search-api-list-enabled")]', NULL, 'Index is in proper table');
+    $this->assertNoLink($this->t('Execute pending tasks'), 'No pending server tasks to execute.');
+
+    // Tests that the "Execute pending tasks" local action is correctly
+    // displayed when there are pending tasks.
+    \Drupal::getContainer()
+      ->get('search_api.server_task_manager')
+      ->add($server, 'deleteItems', $index, array(''));
+    // Due to an (apparent) Core bug we need to clear the cache, otherwise the
+    // "local actions" block gets displayed from cache (without the link). See
+    // #2722237.
+    \Drupal::cache('render')->invalidateAll();
+    $this->drupalGet($this->overviewPageUrl);
+    $this->assertLink($this->t('Execute pending tasks'), 0, '"Execute pending tasks" link displayed.');
   }
 
   /**
diff --git a/tests/src/Kernel/ServerTaskTest.php b/tests/src/Kernel/ServerTaskTest.php
index be1bff8..2c589a5 100644
--- a/tests/src/Kernel/ServerTaskTest.php
+++ b/tests/src/Kernel/ServerTaskTest.php
@@ -6,6 +6,7 @@
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\search_api\Entity\Index;
 use Drupal\search_api\Entity\Server;
+use Drupal\search_api\IndexInterface;
 use Drupal\search_api\SearchApiException;
 
 /**
@@ -104,6 +105,9 @@ public function setUp() {
     $this->index->save();
 
     $this->serverTaskManager = $this->container->get('search_api.server_task_manager');
+
+    // Reset the list of called backend methods.
+    $this->getCalledBackendMethods();
   }
 
   /**
@@ -118,11 +122,11 @@ public function testAddIndex() {
     // Set exception for addIndex() and reset the list of successful backend
     // method calls.
     $this->state->set('search_api_test_backend.exception.addIndex', TRUE);
-    $this->getCalledServerMethods();
+    $this->getCalledBackendMethods();
 
     // Try to add the index.
     $this->server->addIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'addIndex correctly threw an exception.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'addIndex correctly threw an exception.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 1) {
       $task_created = $tasks[0]->type === 'addIndex';
@@ -134,7 +138,7 @@ public function testAddIndex() {
 
     // Check whether other task-system-integrated methods now fail, too.
     $this->server->updateIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'updateIndex was not executed.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'updateIndex was not executed.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 2) {
       $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
@@ -150,21 +154,19 @@ public function testAddIndex() {
     $this->state->set('search_api_test_backend.exception.addIndex', FALSE);
     search_api_cron();
     $this->assertEquals(array(), $this->getServerTasks(), 'Server tasks were correctly executed.');
-    $this->assertEquals(array('addIndex', 'updateIndex'), $this->getCalledServerMethods(), 'Right methods were called during task execution.');
+    $this->assertEquals(array('addIndex', 'updateIndex'), $this->getCalledBackendMethods(), 'Right methods were called during task execution.');
   }
 
   /**
    * Tests task system integration for the server's updateIndex() method.
    */
   public function testUpdateIndex() {
-    // Set exception for updateIndex() and reset the list of successful backend
-    // method calls.
+    // Set exception for updateIndex().
     $this->state->set('search_api_test_backend.exception.updateIndex', TRUE);
-    $this->getCalledServerMethods();
 
     // Try to update the index.
     $this->server->updateIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'updateIndex correctly threw an exception.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'updateIndex correctly threw an exception.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 1) {
       $task_created = $tasks[0]->type === 'updateIndex';
@@ -176,7 +178,7 @@ public function testUpdateIndex() {
 
     // Check whether other task-system-integrated methods now fail, too.
     $this->server->deleteAllIndexItems($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'deleteAllIndexItems was not executed.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'deleteAllIndexItems was not executed.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 2) {
       $this->assertTrue(TRUE, "Second task ('deleteAllIndexItems') was added.");
@@ -198,24 +200,22 @@ public function testUpdateIndex() {
       'indexItems',
     );
     $this->assertEquals(array(), $this->getServerTasks(), 'Server tasks were correctly executed.');
-    $this->assertEquals($expected_methods, $this->getCalledServerMethods(), 'Right methods were called during task execution.');
+    $this->assertEquals($expected_methods, $this->getCalledBackendMethods(), 'Right methods were called during task execution.');
   }
 
   /**
    * Tests task system integration for the server's removeIndex() method.
    */
   public function testRemoveIndex() {
-    // Set exception for updateIndex() and removeIndex(), and reset the list of
-    // successful backend method calls.
+    // Set exception for updateIndex() and removeIndex().
     $this->state->set('search_api_test_backend.exception.updateIndex', TRUE);
     $this->state->set('search_api_test_backend.exception.removeIndex', TRUE);
-    $this->getCalledServerMethods();
 
     // First try to update the index and fail. Then try to remove it and check
     // that the tasks were set correctly.
     $this->server->updateIndex($this->index);
     $this->server->removeIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'updateIndex and removeIndex correctly threw exceptions.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'updateIndex and removeIndex correctly threw exceptions.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 1) {
       $task_created = $tasks[0]->type === 'removeIndex';
@@ -235,7 +235,7 @@ public function testRemoveIndex() {
       $expected_message = new FormattableMarkup('Could not index items on index %index because pending server tasks could not be executed.', $args);
       $this->assertEquals($expected_message, $e->getMessage(), 'Pending server tasks prevented indexing of items.');
     }
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'indexItems was not executed.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'indexItems was not executed.');
     $tasks = $this->getServerTasks();
     $this->assertEquals(1, count($tasks), 'No task added for indexItems.');
 
@@ -244,21 +244,19 @@ public function testRemoveIndex() {
     $this->state->set("search_api_test_backend.exception.removeIndex", FALSE);
     search_api_cron();
     $this->assertEquals(array(), $this->getServerTasks(), 'Server tasks were correctly executed.');
-    $this->assertEquals(array('removeIndex'), $this->getCalledServerMethods(), 'Right methods were called during task execution.');
+    $this->assertEquals(array('removeIndex'), $this->getCalledBackendMethods(), 'Right methods were called during task execution.');
   }
 
   /**
    * Tests task system integration for the server's deleteItems() method.
    */
   public function testDeleteItems() {
-    // Set exception for deleteItems() and reset the list of successful
-    // backend method calls.
+    // Set exception for deleteItems().
     $this->state->set('search_api_test_backend.exception.deleteItems', TRUE);
-    $this->getCalledServerMethods();
 
     // Try to update the index.
     $this->server->deleteItems($this->index, array());
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'deleteItems correctly threw an exception.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'deleteItems correctly threw an exception.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 1) {
       $task_created = $tasks[0]->type === 'deleteItems';
@@ -270,7 +268,7 @@ public function testDeleteItems() {
 
     // Check whether other task-system-integrated methods now fail, too.
     $this->server->updateIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'updateIndex was not executed.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'updateIndex was not executed.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 2) {
       $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
@@ -286,21 +284,19 @@ public function testDeleteItems() {
     $this->state->set('search_api_test_backend.exception.deleteItems', FALSE);
     search_api_cron();
     $this->assertEquals(array(), $this->getServerTasks(), 'Server tasks were correctly executed.');
-    $this->assertEquals(array('deleteItems', 'updateIndex'), $this->getCalledServerMethods(), 'Right methods were called during task execution.');
+    $this->assertEquals(array('deleteItems', 'updateIndex'), $this->getCalledBackendMethods(), 'Right methods were called during task execution.');
   }
 
   /**
    * Tests task system integration for the deleteAllIndexItems() method.
    */
   public function testDeleteAllIndexItems() {
-    // Set exception for deleteAllIndexItems() and reset the list of successful
-    // backend method calls.
+    // Set exception for deleteAllIndexItems().
     $this->state->set('search_api_test_backend.exception.deleteAllIndexItems', TRUE);
-    $this->getCalledServerMethods();
 
     // Try to update the index.
     $this->server->deleteAllIndexItems($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'deleteAllIndexItems correctly threw an exception.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'deleteAllIndexItems correctly threw an exception.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 1) {
       $task_created = $tasks[0]->type === 'deleteAllIndexItems';
@@ -312,7 +308,7 @@ public function testDeleteAllIndexItems() {
 
     // Check whether other task-system-integrated methods now fail, too.
     $this->server->updateIndex($this->index);
-    $this->assertEquals(array(), $this->getCalledServerMethods(), 'updateIndex was not executed.');
+    $this->assertEquals(array(), $this->getCalledBackendMethods(), 'updateIndex was not executed.');
     $tasks = $this->getServerTasks();
     if (count($tasks) == 2) {
       $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
@@ -334,7 +330,106 @@ public function testDeleteAllIndexItems() {
       'indexItems',
     );
     $this->assertEquals(array(), $this->getServerTasks(), 'Server tasks were correctly executed.');
-    $this->assertEquals($expected_methods, $this->getCalledServerMethods(), 'Right methods were called during task execution.');
+    $this->assertEquals($expected_methods, $this->getCalledBackendMethods(), 'Right methods were called during task execution.');
+  }
+
+  /**
+   * Verifies that no more than 100 items will be executed at once.
+   */
+  public function testTaskCountLimit() {
+    // Create 100 tasks.
+    for ($i = 0; $i < 101; ++$i) {
+      $this->serverTaskManager->add($this->server, 'deleteItems', $this->index, array(''));
+    }
+
+    // Verify that a new operation cannot be executed.
+    $this->server->updateIndex($this->index);
+
+    $methods = $this->getCalledBackendMethods();
+    $this->assertCount(100, $methods, '100 pending tasks were executed upon new operation.');
+    $filter = function ($method) { return $method != 'deleteItems'; };
+    $this->assertEmpty(array_filter($methods, $filter), 'The new operation was not executed.');
+    $this->assertEquals(2, $this->serverTaskManager->getCount($this->server), 'A task was created for the new operation.');
+  }
+
+  /**
+   * Tests the correct automatic removal of tasks upon certain operations.
+   */
+  public function testAutomaticTaskRemoval() {
+    // Create a second server and index and add tasks for them.
+    $server2 = Server::create(array(
+      'name' => 'Test Server 2',
+      'id' => 'test_server_2',
+      'status' => 1,
+      'backend' => 'search_api_test_backend',
+    ));
+    $server2->save();
+    $this->serverTaskManager->add($server2, 'removeIndex', $this->index);
+
+    $index_values = $this->index->toArray();
+    unset($index_values['uuid']);
+    $index_values['id'] = 'test_index_2';
+    $index2 = Index::create($index_values);
+    $index2->save();
+    // Reset the called backend methods.
+    $this->getCalledBackendMethods();
+
+    // Verify that adding an index ignores all tasks related to that index.
+    $this->addTasks($index2);
+    $this->server->addIndex($this->index);
+    $this->assertEquals(array('addIndex', 'addIndex'), $this->getCalledBackendMethods(), 'Re-adding an index ignored all its tasks.');
+    $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+    $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+    // Verify that removing an index ignores all tasks related to that index.
+    $this->addTasks($index2);
+    $this->server->removeIndex($this->index);
+    $this->assertEquals(array('addIndex', 'removeIndex'), $this->getCalledBackendMethods(), 'Removing an index ignored all its tasks.');
+    $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+    $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+    // Verify that deleting all of an index's items ignores all other deletion
+    // tasks related to that index.
+    $this->addTasks($index2);
+    $this->server->deleteAllIndexItems($this->index);
+    $this->assertEquals(array('addIndex', 'removeIndex', 'addIndex', 'updateIndex', 'deleteAllIndexItems'), $this->getCalledBackendMethods(), 'Deleting all items of an index ignored all its deletion tasks.');
+    $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+    $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+    // Verify that removing all items from the server automatically removes all
+    // item deletion tasks as well.
+    $this->addTasks($index2);
+    $this->server->deleteAllItems();
+    // deleteAllIndexItems() is called twice – once for each index.
+    $this->assertEquals(array('deleteAllIndexItems', 'deleteAllIndexItems'), $this->getCalledBackendMethods(), "Deleting all items from a server didn't execute any tasks.");
+    $this->assertEquals(4, $this->serverTaskManager->getCount($this->server), 'Deleting all items from a server removed all its item deletion tasks.');
+    $this->assertEquals(5, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+    // Verify that deleting a server also deletes all of its tasks.
+    $this->addTasks($index2);
+    $this->state->set('search_api_test_backend.exception.addIndex', TRUE);
+    $this->state->set('search_api_test_backend.exception.updateIndex', TRUE);
+    $this->state->set('search_api_test_backend.exception.removeIndex', TRUE);
+    $this->state->set('search_api_test_backend.exception.deleteItems', TRUE);
+    $this->state->set('search_api_test_backend.exception.deleteAllIndexItems', TRUE);
+    $this->server->delete();
+    $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'Upon server deletion, all of its tasks were deleted, too.');
+    $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+  }
+
+  /**
+   * Adds one task of each type for this test's server.
+   *
+   * @param \Drupal\search_api\IndexInterface $second_index
+   *   A second index, for which one additional "addIndex" task is created.
+   */
+  protected function addTasks(IndexInterface $second_index) {
+    $this->serverTaskManager->add($this->server, 'addIndex', $second_index);
+    $this->serverTaskManager->add($this->server, 'removeIndex', $this->index);
+    $this->serverTaskManager->add($this->server, 'addIndex', $this->index);
+    $this->serverTaskManager->add($this->server, 'updateIndex', $this->index);
+    $this->serverTaskManager->add($this->server, 'deleteItems', $this->index, array());
+    $this->serverTaskManager->add($this->server, 'deleteAllIndexItems', $this->index);
   }
 
   /**
@@ -347,7 +442,7 @@ public function testDeleteAllIndexItems() {
    * @return string[]
    *   The methods called on the test server since the last reset.
    */
-  protected function getCalledServerMethods($reset = TRUE) {
+  protected function getCalledBackendMethods($reset = TRUE) {
     $key = 'search_api_test_backend.methods_called.' . $this->server->id();
     $methods_called = $this->state->get($key, array());
     if ($reset) {
