diff --git a/search_api.install b/search_api.install
index 2777f88..f998ca7 100644
--- a/search_api.install
+++ b/search_api.install
@@ -121,6 +121,7 @@ function search_api_requirements($phase) {
       );
     }
 
+    /** @var \Drupal\search_api\ServerInterface[] $servers */
     $servers = Server::loadMultiple();
     $unavailable_servers = array();
     foreach ($servers as $server) {
@@ -143,6 +144,21 @@ function search_api_requirements($phase) {
       );
     }
 
+    $count = \Drupal::getContainer()
+      ->get('search_api.server_task_manager')
+      ->getCount();
+    if ($count) {
+      $requirements['search_api_pending_tasks'] = array(
+        'title' => t('Search API'),
+        'value' => \Drupal::translation()->formatPlural(
+          $count,
+          'There is @count pending task for a Search API server.',
+          'There are @count pending tasks for Search API servers.'
+        ),
+        'severity' => REQUIREMENT_WARNING,
+      );
+    }
+
     return $requirements;
   }
   return array();
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..f4f89bd 100644
--- a/src/Form/ServerStatusForm.php
+++ b/src/Form/ServerStatusForm.php
@@ -24,6 +24,31 @@ public function getFormId() {
   public function buildForm(array $form, FormStateInterface $form_state, ServerInterface $server = NULL) {
     $form['#server'] = $server;
 
+    $count = \Drupal::getContainer()
+      ->get('search_api.server_task_manager')
+      ->getCount($server);
+    if ($count) {
+      $status = $this->formatPlural(
+        $count,
+        '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(),
+      );
+    }
+
     $form['actions']['#type'] = 'actions';
     $form['actions']['clear'] = array(
       '#type' => 'submit',
@@ -37,10 +62,20 @@ public function buildForm(array $form, FormStateInterface $form_state, ServerInt
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    // Redirect to the "Clear server" confirmation form.
     /** @var \Drupal\search_api\ServerInterface $server */
     $server = $form['#server'];
-    $form_state->setRedirect('entity.search_api_server.clear', array('search_api_server' => $server->id()));
+
+    if ($form_state->getValue('op') == $this->t('Execute tasks now')) {
+      // Set the "server task execution" batch.
+      \Drupal::getContainer()
+        ->get('search_api.server_task_manager')
+        ->setExecuteBatch($server);
+    }
+    else {
+      // Redirect to the "Clear server" confirmation form.
+      $parameters = array('search_api_server' => $server->id());
+      $form_state->setRedirect('entity.search_api_server.clear', $parameters);
+    }
   }
 
 }
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..5c56332 100644
--- a/src/Task/ServerTaskManager.php
+++ b/src/Task/ServerTaskManager.php
@@ -3,7 +3,9 @@
 namespace Drupal\search_api\Task;
 
 use Drupal\Core\Database\Connection;
+use Drupal\Core\Database\Query\SelectInterface;
 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 +15,8 @@
  */
 class ServerTaskManager implements ServerTaskManagerInterface {
 
+  use StringTranslationTrait;
+
   /**
    * The database connection.
    *
@@ -43,36 +47,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 +100,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 +117,225 @@ 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.
+   *
+   * @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);
+    if ($finished) {
+      $context['finished'] = 1;
+    }
+    else {
+      $count = $this->getCount($server);
+      if ($count == $context['results']['executed']) {
+        $context['finished'] = 1;
+      }
+      else {
+        $context['results']['executed'] = $count;
+        $context['finished'] = $count / $context['results']['total'];
+        $diff = $context['results']['total'] - $count;
+        $context['message'] = $this->formatPlural($diff, 'Executed @count task.', 'Executed @count tasks.');
+      }
+    }
+  }
+
+  /**
+   * 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 +355,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 +366,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/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) {
