diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml
index cce9d1c7260d870f3d2e8b230c58da57f3b8de7a..f831d0741ad669d6f75b13bf6e1d2eb5cc442283 100644
--- a/automatic_updates.services.yml
+++ b/automatic_updates.services.yml
@@ -46,6 +46,14 @@ services:
       - '@automatic_updates.path_locator'
     tags:
       - { name: event_subscriber }
+  automatic_updates.pending_updates_validator:
+    class: Drupal\automatic_updates\Validator\PendingUpdatesValidator
+    arguments:
+      - '%app.root%'
+      - '@update.post_update_registry'
+      - '@string_translation'
+    tags:
+      - { name: event_subscriber }
   automatic_updates.validator.file_system_permissions:
     class: Drupal\automatic_updates\Validator\WritableFileSystemValidator
     arguments:
diff --git a/src/Validator/PendingUpdatesValidator.php b/src/Validator/PendingUpdatesValidator.php
new file mode 100644
index 0000000000000000000000000000000000000000..02c3d686a0f7844d6e98a66cdf80937426d5fd54
--- /dev/null
+++ b/src/Validator/PendingUpdatesValidator.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\automatic_updates\Validator;
+
+use Drupal\automatic_updates\AutomaticUpdatesEvents;
+use Drupal\automatic_updates\Event\UpdateEvent;
+use Drupal\automatic_updates\Validation\ValidationResult;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Drupal\Core\Update\UpdateRegistry;
+use Drupal\Core\Url;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Validates that there are no pending database updates.
+ */
+class PendingUpdatesValidator implements EventSubscriberInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The Drupal root.
+   *
+   * @var string
+   */
+  protected $appRoot;
+
+  /**
+   * The update registry service.
+   *
+   * @var \Drupal\Core\Update\UpdateRegistry
+   */
+  protected $updateRegistry;
+
+  /**
+   * Constructs an PendingUpdatesValidator object.
+   *
+   * @param string $app_root
+   *   The Drupal root.
+   * @param \Drupal\Core\Update\UpdateRegistry $update_registry
+   *   The update registry service.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
+   *   The translation service.
+   */
+  public function __construct(string $app_root, UpdateRegistry $update_registry, TranslationInterface $translation) {
+    $this->appRoot = $app_root;
+    $this->updateRegistry = $update_registry;
+    $this->setStringTranslation($translation);
+  }
+
+  /**
+   * Validates that there are no pending database updates.
+   *
+   * @param \Drupal\automatic_updates\Event\UpdateEvent $event
+   *   The update event.
+   */
+  public function checkPendingUpdates(UpdateEvent $event) {
+    require_once $this->appRoot . '/core/includes/install.inc';
+    require_once $this->appRoot . '/core/includes/update.inc';
+
+    drupal_load_updates();
+    $hook_updates = update_get_update_list();
+    $post_updates = $this->updateRegistry->getPendingUpdateFunctions();
+
+    if ($hook_updates || $post_updates) {
+      $message = $this->t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [
+        ':update' => Url::fromRoute('system.db_update')->toString(),
+      ]);
+      $error = ValidationResult::createError([$message]);
+      $event->addValidationResult($error);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      AutomaticUpdatesEvents::PRE_START => 'checkPendingUpdates',
+      AutomaticUpdatesEvents::READINESS_CHECK => 'checkPendingUpdates',
+    ];
+  }
+
+}
diff --git a/tests/fixtures/db_update.php b/tests/fixtures/db_update.php
new file mode 100644
index 0000000000000000000000000000000000000000..dcebeff41012ad40820b3e17880b40c74f31eb3c
--- /dev/null
+++ b/tests/fixtures/db_update.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Contains a fake database update function for testing.
+ */
+
+/**
+ * Here is a fake update.
+ */
+function automatic_updates_update_50000() {
+}
diff --git a/tests/fixtures/post_update.php b/tests/fixtures/post_update.php
new file mode 100644
index 0000000000000000000000000000000000000000..a77909b6d4db212fd19041e4a379d68dcfae4b5c
--- /dev/null
+++ b/tests/fixtures/post_update.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * @file
+ * Contains a fake post-update function for testing.
+ */
+
+/**
+ * Here is a fake post-update.
+ */
+function automatic_updates_post_update_test() {
+}
diff --git a/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..a304b7aad798a4121982ba98c158f37c1f5f4e98
--- /dev/null
+++ b/tests/src/Kernel/ReadinessValidation/PendingUpdatesValidatorTest.php
@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Kernel\ReadinessValidation;
+
+use Drupal\automatic_updates\Event\UpdateEvent;
+use Drupal\automatic_updates\Validation\ValidationResult;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\automatic_updates\Traits\ValidationTestTrait;
+
+/**
+ * @covers \Drupal\automatic_updates\Validator\PendingUpdatesValidator
+ *
+ * @group automatic_updates
+ */
+class PendingUpdatesValidatorTest extends KernelTestBase {
+
+  use ValidationTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'automatic_updates',
+    'package_manager',
+    'system',
+    'update',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    // Make the update system think that all of System's post-update functions
+    // have run. Since kernel tests don't normally install modules and register
+    // their updates, we need to do this so that the validator is being tested
+    // from a clean, fully up-to-date state.
+    $updates = $this->container->get('update.post_update_registry')
+      ->getPendingUpdateFunctions();
+
+    $this->container->get('keyvalue')
+      ->get('post_update')
+      ->set('existing_updates', $updates);
+  }
+
+  /**
+   * Tests that no error is raised if there are no pending updates.
+   */
+  public function testNoPendingUpdates(): void {
+    $event = new UpdateEvent();
+    $this->container->get('automatic_updates.pending_updates_validator')
+      ->checkPendingUpdates($event);
+    $this->assertEmpty($event->getResults());
+  }
+
+  /**
+   * Tests that an error is raised if there are pending schema updates.
+   */
+  public function testPendingUpdateHook(): void {
+    require __DIR__ . '/../../../fixtures/db_update.php';
+
+    $this->container->get('keyvalue')
+      ->get('system.schema')
+      ->set('automatic_updates', \Drupal::CORE_MINIMUM_SCHEMA_VERSION);
+
+    $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']);
+
+    $event = new UpdateEvent();
+    $this->container->get('automatic_updates.pending_updates_validator')
+      ->checkPendingUpdates($event);
+    $this->assertValidationResultsEqual([$result], $event->getResults());
+  }
+
+  /**
+   * Tests that an error is raised if there are pending post-updates.
+   */
+  public function testPendingPostUpdate(): void {
+    require __DIR__ . '/../../../fixtures/post_update.php';
+
+    $result = ValidationResult::createError(['Some modules have database schema updates to install. You should run the <a href="/update.php">database update script</a> immediately.']);
+
+    $event = new UpdateEvent();
+    $this->container->get('automatic_updates.pending_updates_validator')
+      ->checkPendingUpdates($event);
+    $this->assertValidationResultsEqual([$result], $event->getResults());
+  }
+
+}