diff --git a/src/BatchProcessor.php b/src/BatchProcessor.php
index 940418929d71974983a5db8b7b305de1ca3d6411..8723b9ce9ccbb818889d2ce6b9f7aa0dc856eb22 100644
--- a/src/BatchProcessor.php
+++ b/src/BatchProcessor.php
@@ -91,7 +91,60 @@ class BatchProcessor {
   }
 
   /**
-   * Finishes the batch job.
+   * Calls the updater's commit() method.
+   *
+   * @param array $context
+   *   The current context of the batch job.
+   *
+   * @see \Drupal\automatic_updates\Updater::commit()
+   */
+  public static function commit(array &$context): void {
+    try {
+      static::getUpdater()->commit();
+    }
+    catch (\Throwable $e) {
+      static::handleException($e, $context);
+    }
+  }
+
+  /**
+   * Calls the updater's clean() method.
+   *
+   * @param array $context
+   *   The current context of the batch job.
+   *
+   * @see \Drupal\automatic_updates\Updater::clean()
+   */
+  public static function clean(array &$context): void {
+    try {
+      static::getUpdater()->clean();
+    }
+    catch (\Throwable $e) {
+      static::handleException($e, $context);
+    }
+  }
+
+  /**
+   * Finishes the stage batch job.
+   *
+   * @param bool $success
+   *   Indicate that the batch API tasks were all completed successfully.
+   * @param array $results
+   *   An array of all the results.
+   * @param array $operations
+   *   A list of the operations that had not been completed by the batch API.
+   */
+  public static function finishStage(bool $success, array $results, array $operations): ?RedirectResponse {
+    if ($success) {
+      return new RedirectResponse(Url::fromRoute('automatic_updates.confirmation_page', [],
+        ['absolute' => TRUE])->toString());
+    }
+    static::handleBatchError($results);
+    return NULL;
+  }
+
+  /**
+   * Finishes the commit batch job.
    *
    * @param bool $success
    *   Indicate that the batch API tasks were all completed successfully.
@@ -100,10 +153,25 @@ class BatchProcessor {
    * @param array $operations
    *   A list of the operations that had not been completed by the batch API.
    */
-  public static function finish(bool $success, array $results, array $operations): ?RedirectResponse {
+  public static function finishCommit(bool $success, array $results, array $operations): ?RedirectResponse {
+
     if ($success) {
-      return new RedirectResponse(Url::fromRoute('automatic_updates.confirmation_page', [], ['absolute' => TRUE])->toString());
+      \Drupal::messenger()->addMessage('Update complete!');
+      // @todo redirect to update.php?
+      return new RedirectResponse(Url::fromRoute('automatic_updates.update_form', [],
+        ['absolute' => TRUE])->toString());
     }
+    static::handleBatchError($results);
+    return NULL;
+  }
+
+  /**
+   * Handles a batch job that finished with errors.
+   *
+   * @param array $results
+   *   The batch results.
+   */
+  protected static function handleBatchError(array $results): void {
     if (isset($results['errors'])) {
       foreach ($results['errors'] as $error) {
         \Drupal::messenger()->addError($error);
@@ -112,7 +180,6 @@ class BatchProcessor {
     else {
       \Drupal::messenger()->addError("Update error");
     }
-    return NULL;
   }
 
 }
diff --git a/src/Form/UpdateReady.php b/src/Form/UpdateReady.php
index cb978f7d20ae3df62bb25f51882237bae107131b..f4dcc94b9e45a28ee2f8c214e58452b38ab9181a 100644
--- a/src/Form/UpdateReady.php
+++ b/src/Form/UpdateReady.php
@@ -3,7 +3,9 @@
 namespace Drupal\automatic_updates\Form;
 
 use Drupal\automatic_updates\AutomaticUpdatesEvents;
+use Drupal\automatic_updates\BatchProcessor;
 use Drupal\automatic_updates\Updater;
+use Drupal\Core\Batch\BatchBuilder;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\State\StateInterface;
@@ -106,15 +108,15 @@ class UpdateReady extends UpdateFormBase {
       $this->state->set('system.maintenance_mode', TRUE);
       // @todo unset after updater. After db update?
     }
-
-    // @todo Should these operations be done in batch.
-    $this->updater->commit();
-    // Clean could be done in another page load or on cron to reduce page time.
-    $this->updater->clean();
-    $this->messenger->addMessage("Update complete!");
-
-    // @todo redirect to update.php?
-    $form_state->setRedirect('automatic_updates.update_form');
+    $batch = (new BatchBuilder())
+      ->setTitle($this->t('Apply updates'))
+      ->setInitMessage($this->t('Preparing to apply updates'))
+      ->addOperation([BatchProcessor::class, 'commit'])
+      ->addOperation([BatchProcessor::class, 'clean'])
+      ->setFinishCallback([BatchProcessor::class, 'finishCommit'])
+      ->toArray();
+
+    batch_set($batch);
   }
 
 }
diff --git a/src/Form/UpdaterForm.php b/src/Form/UpdaterForm.php
index 599c7257e0a9453a1e9c0a9881c217d7c2b21aca..98d57ed320dd9bddcc2ec1a471b1e7d86c6033af 100644
--- a/src/Form/UpdaterForm.php
+++ b/src/Form/UpdaterForm.php
@@ -240,7 +240,7 @@ class UpdaterForm extends UpdateFormBase {
       ->addOperation([BatchProcessor::class, 'stageProjectVersions'], [
         $form_state->getValue('update_version'),
       ])
-      ->setFinishCallback([BatchProcessor::class, 'finish'])
+      ->setFinishCallback([BatchProcessor::class, 'finishStage'])
       ->toArray();
 
     batch_set($batch);
diff --git a/tests/src/Build/AttendedCoreUpdateTest.php b/tests/src/Build/AttendedCoreUpdateTest.php
index 64fd82758a1858764a6f8ddbd4fd6325d4c3477b..23cfc81147f7c53fa3b771761e041e892c4b1a5a 100644
--- a/tests/src/Build/AttendedCoreUpdateTest.php
+++ b/tests/src/Build/AttendedCoreUpdateTest.php
@@ -122,6 +122,7 @@ class AttendedCoreUpdateTest extends AttendedUpdateTestBase {
     $this->waitForBatchJob();
     $assert_session->pageTextContains('Ready to update');
     $page->pressButton('Continue');
+    $this->waitForBatchJob();
     // @todo This message isn't showing up, for some reason. Figure out what the
     // eff is going on.
     // $assert_session->pageTextContains('Update complete!');