diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 7d905cd..f0e32da 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -399,6 +399,7 @@ function _batch_next_set() {
  */
 function _batch_finished() {
   $batch = &batch_get();
+  $batch_finished_redirect = NULL;
 
   // Execute the 'finished' callbacks for each batch set, if defined.
   foreach ($batch['sets'] as $batch_set) {
@@ -410,7 +411,13 @@ function _batch_finished() {
       if (is_callable($batch_set['finished'])) {
         $queue = _batch_queue($batch_set);
         $operations = $queue->getAllItems();
-        call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)));
+        $batch_set_result = call_user_func_array($batch_set['finished'], array($batch_set['success'], $batch_set['results'], $operations, \Drupal::service('date.formatter')->formatInterval($batch_set['elapsed'] / 1000)));
+        // If a batch 'finished' callback requested a redirect after the batch
+        // is complete, save that for later use. If more than one batch set
+        // returned a redirect, the last one is used.
+        if ($batch_set_result instanceof RedirectResponse) {
+          $batch_finished_redirect = $batch_set_result;
+        }
       }
     }
   }
@@ -441,8 +448,13 @@ function _batch_finished() {
       \Drupal::request()->query->set('destination', $_batch['destination']);
     }
 
-    // Determine the target path to redirect to.
-    if (!isset($_batch['form_state'])) {
+    // Determine the target path to redirect to. If a batch 'finished' callback
+    // returned a redirect response object, use that. Otherwise, fall back on
+    // the form redirection.
+    if (isset($batch_finished_redirect)) {
+      return $batch_finished_redirect;
+    }
+    elseif (!isset($_batch['form_state'])) {
       $_batch['form_state'] = new FormState();
     }
     if ($_batch['form_state']->getRedirect() === NULL) {
diff --git a/core/includes/form.inc b/core/includes/form.inc
index eb028e3..e945da8 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -748,7 +748,14 @@ function batch_set($batch_definition) {
  *
  * @param \Drupal\Core\Url|string $redirect
  *   (optional) Either path or Url object to redirect to when the batch has
- *   finished processing.
+ *   finished processing. Note that to simply force a batch to (conditionally)
+ *   redirect to a custom location after it is finished processing but to
+ *   otherwise allow the standard form API batch handling to occur, it is not
+ *   necessary to call batch_process() and use this parameter. Instead, make
+ *   the batch 'finished' callback return an instance of
+ *   \Symfony\Component\HttpFoundation\RedirectResponse, which will be used
+ *   automatically by the standard batch processing pipeline (and which takes
+ *   precedence over this parameter).
  * @param \Drupal\Core\Url $url
  *   (optional - should only be used for separate scripts like update.php)
  *   URL of the batch processing page.
diff --git a/core/modules/system/src/Tests/Batch/ProcessingTest.php b/core/modules/system/src/Tests/Batch/ProcessingTest.php
index 1383106..7726f56 100644
--- a/core/modules/system/src/Tests/Batch/ProcessingTest.php
+++ b/core/modules/system/src/Tests/Batch/ProcessingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\system\Tests\Batch;
 
+use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -21,23 +22,30 @@ class ProcessingTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('batch_test');
+  public static $modules = array('batch_test', 'test_page_test');
 
   /**
    * Tests batches triggered outside of form submission.
    */
   function testBatchNoForm() {
     // Displaying the page triggers batch 1.
-    $this->drupalGet('batch-test/no-form');
+//    $this->drupalGet('batch-test/no-form');
+//    $this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 2 performed successfully.');
+//    $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
+//    $this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
+
+    // Displaying the page triggers batch 1.
+    $this->drupalGet('batch-test/finish-redirect');
     $this->assertBatchMessages($this->_resultMessages('batch_1'), 'Batch for step 2 performed successfully.');
     $this->assertEqual(batch_test_stack(), $this->_resultStack('batch_1'), 'Execution order was correct.');
     $this->assertText('Redirection successful.', 'Redirection after batch execution is correct.');
+    $this->assertUrl(Url::fromRoute('test_page_test.test_page'));
   }
 
   /**
    * Tests batches defined in a form submit handler.
    */
-  function testBatchForm() {
+  function ptestBatchForm() {
     // Batch 0: no operation.
     $edit = array('batch' => 'batch_0');
     $this->drupalPostForm('batch-test', $edit, 'Submit');
@@ -76,7 +84,7 @@ function testBatchForm() {
   /**
    * Tests batches defined in a multistep form.
    */
-  function testBatchFormMultistep() {
+  function ptestBatchFormMultistep() {
     $this->drupalGet('batch-test/multistep');
     $this->assertText('step 1', 'Form is displayed in step 1.');
 
@@ -96,7 +104,7 @@ function testBatchFormMultistep() {
   /**
    * Tests batches defined in different submit handlers on the same form.
    */
-  function testBatchFormMultipleBatches() {
+  function ptestBatchFormMultipleBatches() {
     // Batches 1, 2 and 3 are triggered in sequence by different submit
     // handlers. Each submit handler modify the submitted 'value'.
     $value = rand(0, 255);
@@ -115,7 +123,7 @@ function testBatchFormMultipleBatches() {
    *
    * Same as above, but the form is submitted through drupal_form_execute().
    */
-  function testBatchFormProgrammatic() {
+  function ptestBatchFormProgrammatic() {
     // Batches 1, 2 and 3 are triggered in sequence by different submit
     // handlers. Each submit handler modify the submitted 'value'.
     $value = rand(0, 255);
@@ -131,7 +139,7 @@ function testBatchFormProgrammatic() {
   /**
    * Test form submission during a batch operation.
    */
-  function testDrupalFormSubmitInBatch() {
+  function ptestDrupalFormSubmitInBatch() {
     // Displaying the page triggers a batch that programmatically submits a
     // form.
     $value = rand(0, 255);
@@ -144,7 +152,7 @@ function testDrupalFormSubmitInBatch() {
    *
    * @see http://drupal.org/node/600836
    */
-  function testBatchLargePercentage() {
+  function ptestBatchLargePercentage() {
     // Displaying the page triggers batch 5.
     $this->drupalGet('batch-test/large-percentage');
     $this->assertBatchMessages($this->_resultMessages('batch_5'), 'Batch for step 2 performed successfully.');
diff --git a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
index 6fc9a8d..8dcc1fa 100644
--- a/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
+++ b/core/modules/system/tests/modules/batch_test/batch_test.callbacks.inc
@@ -6,6 +6,8 @@
  */
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Url;
+use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Implements callback_batch_operation().
@@ -126,6 +128,16 @@ function _batch_test_finished_1($success, $results, $operations) {
 /**
  * Implements callback_batch_finished().
  *
+ * Triggers 'finished' callback for batch 1.
+ */
+function _batch_test_finished_1_finished($success, $results, $operations) {
+  _batch_test_finished_helper(1, $success, $results, $operations);
+  return new RedirectResponse(Url::fromRoute('test_page_test.test_page')->toString());
+}
+
+/**
+ * Implements callback_batch_finished().
+ *
  * Triggers 'finished' callback for batch 2.
  */
 function _batch_test_finished_2($success, $results, $operations) {
diff --git a/core/modules/system/tests/modules/batch_test/batch_test.routing.yml b/core/modules/system/tests/modules/batch_test/batch_test.routing.yml
index 638d6fe..5b5a117 100644
--- a/core/modules/system/tests/modules/batch_test/batch_test.routing.yml
+++ b/core/modules/system/tests/modules/batch_test/batch_test.routing.yml
@@ -31,6 +31,14 @@ batch_test.no_form:
   requirements:
     _access: 'TRUE'
 
+batch_test.finish_redirect:
+  path: '/batch-test/finish-redirect'
+  defaults:
+    _controller: '\Drupal\batch_test\Controller\BatchTestController::testFinishRedirect'
+    _title: 'Simple page with finish redirect call'
+  requirements:
+    _access: 'TRUE'
+
 batch_test.test_form:
   path: '/batch-test'
   defaults:
diff --git a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
index 19ea6e9..a5707ea 100644
--- a/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
+++ b/core/modules/system/tests/modules/batch_test/src/Controller/BatchTestController.php
@@ -73,6 +73,21 @@ public function testNoForm() {
   }
 
   /**
+   * Fires a batch process without a form submission and a finish redirect.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|null
+   *   A redirect response if the batch is progressive. No return value otherwise.
+   */
+  public function testFinishRedirect() {
+    batch_test_stack(NULL, TRUE);
+
+    $batch = _batch_test_batch_1();
+     $batch['finished'] = '_batch_test_finished_1_finished';
+    batch_set($batch);
+    return batch_process('batch-test/redirect');
+  }
+
+  /**
    * Submits the 'Chained' form programmatically.
    *
    * Programmatic form: the page submits the 'Chained' form through
