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 9c72685..c060c0b 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -747,7 +747,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.
