diff --git a/core/modules/auto_updates/auto_updates.info.yml b/core/modules/auto_updates/auto_updates.info.yml
new file mode 100644
index 0000000000..1914ec34e7
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.info.yml
@@ -0,0 +1,7 @@
+name: 'Automatic Updates'
+type: module
+description: 'Experimental module to develop automatic updates. Currently the module does not provide update functionality.'
+configure: auto_updates.settings
+package: Core (Experimental)
+version: VERSION
+hidden: true
diff --git a/core/modules/auto_updates/auto_updates.install b/core/modules/auto_updates/auto_updates.install
new file mode 100644
index 0000000000..600babcf92
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.install
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Automatic Updates.
+ */
+
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+
+/**
+ * Implements hook_requirements().
+ */
+function auto_updates_requirements($phase) {
+  if ($phase !== 'runtime') {
+    return [];
+  }
+
+  /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */
+  $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager');
+  if (!$checker_manager->isEnabled()) {
+    return [];
+  }
+  $requirements['auto_updates_readiness']['title'] = t('Update readiness checks');
+  $readiness_check = Url::fromRoute('auto_updates.update_readiness');
+  $last_check_timestamp = $checker_manager->getTimestamp();
+  if ($last_check_timestamp === NULL) {
+    $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_ERROR;
+    $requirements['auto_updates_readiness']['value'] = t('Your site has never checked if it is ready to apply <a href=":readiness_checks">automatic updates</a>.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']);
+    if ($readiness_check->access()) {
+      $requirements['auto_updates_readiness']['description'] = t('<a href=":link">Run readiness checks</a> manually.', [
+        ':link' => $readiness_check->toString(),
+      ]);
+    }
+  }
+  elseif (!$checker_manager->hasRunRecently()) {
+    $requirements['auto_updates_readiness']['severity'] = REQUIREMENT_ERROR;
+    $time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp);
+    $requirements['auto_updates_readiness']['value'] = t('Your site has not recently checked if it is ready to apply <a href=":readiness_checks">automatic updates</a>.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']);
+    if ($readiness_check->access()) {
+      $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago. <a href=":link">Run readiness checks</a> manually.', [
+        '@time' => $time_ago,
+        ':link' => $readiness_check->toString(),
+      ]);
+    }
+    else {
+      $requirements['auto_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]);
+    }
+  }
+  else {
+    $error_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::ERROR);
+    $warning_results = $checker_manager->getResults(ReadinessCheckerManagerInterface::WARNING);
+    $checker_results = array_merge($error_results, $warning_results);
+    if (!empty($checker_results)) {
+      $requirements['auto_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
+      $requirements['auto_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:');
+      $requirements['auto_updates_readiness']['description'] = [
+        '#theme' => 'item_list',
+        '#items' => $checker_results,
+      ];
+    }
+    else {
+      $requirements['auto_updates_readiness'] += [
+        'severity' => REQUIREMENT_OK,
+        'value' => t('Your site is ready for <a href=":readiness_checks">automatic updates</a>.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']),
+      ];
+    }
+  }
+  return $requirements;
+}
+
+/**
+ * Implements hook_install().
+ */
+function auto_updates_install($is_syncing) {
+  /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */
+  $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager');
+  $checker_manager->run();
+}
diff --git a/core/modules/auto_updates/auto_updates.links.menu.yml b/core/modules/auto_updates/auto_updates.links.menu.yml
new file mode 100644
index 0000000000..4505736268
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.links.menu.yml
@@ -0,0 +1,5 @@
+auto_updates.settings:
+  title: 'Automatic updates'
+  route_name: auto_updates.settings
+  description: 'Configure automatic update settings.'
+  parent: system.admin_config_system
diff --git a/core/modules/auto_updates/auto_updates.module b/core/modules/auto_updates/auto_updates.module
new file mode 100644
index 0000000000..b75d246926
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.module
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Automatic Updates.
+ */
+
+use Drupal\Core\Url;
+use Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+
+/**
+ * Implements hook_page_top().
+ */
+function auto_updates_page_top(array &$page_top) {
+  /** @var \Drupal\Core\Routing\AdminContext $admin_context */
+  $admin_context = \Drupal::service('router.admin_context');
+  $route_match = \Drupal::routeMatch();
+  if ($admin_context->isAdminRoute($route_match->getRouteObject()) && \Drupal::currentUser()->hasPermission('administer site configuration')) {
+    $disabled_routes = [
+      'update.theme_update',
+      'system.theme_install',
+      'update.module_update',
+      'update.module_install',
+      'update.status',
+      'update.report_update',
+      'update.report_install',
+      'update.settings',
+      'system.status',
+      'update.confirmation_page',
+    ];
+    // These routes don't need additional nagging.
+    if (in_array(\Drupal::routeMatch()->getRouteName(), $disabled_routes, TRUE)) {
+      return;
+    }
+    /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */
+    $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager');
+    if (!$checker_manager->hasRunRecently()) {
+      $readiness_settings = Url::fromRoute('auto_updates.settings');
+      \Drupal::messenger()->addError(t('Your site has not recently run an update readiness check. <a href=":url">Administer automatic updates</a> and run readiness checks manually.', [
+        ':url' => $readiness_settings->toString(),
+      ]));
+    }
+    $results = $checker_manager->getResults(ReadinessCheckerManagerInterface::ERROR);
+    if ($results) {
+      \Drupal::messenger()->addError(t('Your site is currently failing readiness checks for automatic updates. It cannot be <a href=":readiness_checks">automatically updated</a> until further action is performed.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/auto-updates#readiness-checks']));
+      foreach ($results as $message) {
+        \Drupal::messenger()->addError($message);
+      }
+    }
+    $results = $checker_manager->getResults(ReadinessCheckerManagerInterface::WARNING);
+    if ($results) {
+      \Drupal::messenger()->addWarning(t('Your site does not pass some readiness checks for automatic updates. Depending on the nature of the failures, it might effect the eligibility for <a href=":readiness_checks">automatic updates</a>.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/auto-updates#readiness-checks']));
+      foreach ($results as $message) {
+        \Drupal::messenger()->addWarning($message);
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_cron().
+ */
+function auto_updates_cron() {
+  $request_time = \Drupal::time()->getRequestTime();
+  /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */
+  $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager');
+  $last_check = $checker_manager->getTimestamp();
+  // Only allow cron to run once every hour.
+  if ($last_check && ($request_time - $last_check) < 3600) {
+    return;
+  }
+  $checker_manager->run();
+}
+
+/**
+ * Implements hook_modules_installed().
+ */
+function auto_updates_modules_installed($modules) {
+  /** @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager */
+  $checker_manager = \Drupal::service('auto_updates.readiness_checker_manager');
+  if ($checker_manager->clearStaleResults()) {
+    $checker_manager->run();
+  }
+}
diff --git a/core/modules/auto_updates/auto_updates.routing.yml b/core/modules/auto_updates/auto_updates.routing.yml
new file mode 100644
index 0000000000..eba68de6bf
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.routing.yml
@@ -0,0 +1,19 @@
+auto_updates.settings:
+  path: '/admin/config/auto_updates'
+  defaults:
+    _form: '\Drupal\auto_updates\Form\SettingsForm'
+    _title: 'Automatic Updates'
+  requirements:
+    _permission: 'administer software updates'
+  options:
+    _admin_route: TRUE
+auto_updates.update_readiness:
+  path: '/admin/config/auto_updates/readiness'
+  defaults:
+    _controller: '\Drupal\auto_updates\Controller\ReadinessCheckerController::run'
+    _title: 'Update readiness checking'
+  requirements:
+    _permission: 'administer software updates'
+    _custom_access: '\Drupal\auto_updates\Controller\ReadinessCheckerController::access'
+  options:
+    _admin_route: TRUE
diff --git a/core/modules/auto_updates/auto_updates.services.yml b/core/modules/auto_updates/auto_updates.services.yml
new file mode 100644
index 0000000000..4b8c52678b
--- /dev/null
+++ b/core/modules/auto_updates/auto_updates.services.yml
@@ -0,0 +1,11 @@
+services:
+  auto_updates.disk_space_checker:
+    class: Drupal\auto_updates\ReadinessChecker\DiskSpace
+    arguments: ['%app.root%']
+    tags:
+      - { name: readiness_checker, category: error}
+  auto_updates.readiness_checker_manager:
+    class: Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManager
+    arguments: ['@keyvalue', '@config.factory', '@datetime.time']
+    tags:
+      - { name: service_collector, tag: readiness_checker, call: addChecker }
diff --git a/core/modules/auto_updates/config/install/auto_updates.settings.yml b/core/modules/auto_updates/config/install/auto_updates.settings.yml
new file mode 100644
index 0000000000..de0b8d7d73
--- /dev/null
+++ b/core/modules/auto_updates/config/install/auto_updates.settings.yml
@@ -0,0 +1 @@
+enable_readiness_checks: true
diff --git a/core/modules/auto_updates/config/schema/auto_updates.schema.yml b/core/modules/auto_updates/config/schema/auto_updates.schema.yml
new file mode 100644
index 0000000000..047a224f44
--- /dev/null
+++ b/core/modules/auto_updates/config/schema/auto_updates.schema.yml
@@ -0,0 +1,7 @@
+auto_updates.settings:
+  type: config_object
+  label: 'Automatic updates settings'
+  mapping:
+    enable_readiness_checks:
+      type: boolean
+      label: 'Enable readiness checks'
diff --git a/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php b/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php
new file mode 100644
index 0000000000..4381d05918
--- /dev/null
+++ b/core/modules/auto_updates/src/Controller/ReadinessCheckerController.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\auto_updates\Controller;
+
+use Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A controller for running Readiness Checkers.
+ *
+ * @internal
+ *   Controller classes are internal.
+ */
+class ReadinessCheckerController extends ControllerBase {
+
+  /**
+   * The readiness checker manager.
+   *
+   * @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface
+   */
+  protected $checkerManager;
+
+  /**
+   * ReadinessCheckerController constructor.
+   *
+   * @param \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker_manager
+   *   The readiness checker manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(ReadinessCheckerManagerInterface $checker_manager, TranslationInterface $string_translation) {
+    $this->checkerManager = $checker_manager;
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('auto_updates.readiness_checker_manager'),
+      $container->get('string_translation')
+    );
+  }
+
+  /**
+   * Run the readiness checkers.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect to the automatic updates settings page.
+   */
+  public function run() {
+    if (!array_filter($this->checkerManager->run())) {
+      $this->messenger()->addStatus($this->t('No issues found. Your site is ready for <a href=":readiness_checks">automatic updates</a>.', [':readiness_checks' => 'https://www.drupal.org/docs/8/update/auto-updates#readiness-checks']));
+    }
+    return $this->redirect('auto_updates.settings');
+  }
+
+  /**
+   * Checks access based on whether the readiness checkers are enabled.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access() {
+    return AccessResult::allowedIf($this->checkerManager->isEnabled());
+  }
+
+}
diff --git a/core/modules/auto_updates/src/Form/SettingsForm.php b/core/modules/auto_updates/src/Form/SettingsForm.php
new file mode 100644
index 0000000000..96f634dfcb
--- /dev/null
+++ b/core/modules/auto_updates/src/Form/SettingsForm.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\auto_updates\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Settings form for Automatic Updates.
+ */
+class SettingsForm extends ConfigFormBase {
+
+  /**
+   * The readiness checker manager.
+   *
+   * @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerManagerInterface
+   */
+  protected $checkerManager;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $instance = parent::create($container);
+    $instance->checkerManager = $container->get('auto_updates.readiness_checker_manager');
+    $instance->dateFormatter = $container->get('date.formatter');
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'auto_updates.settings',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'auto_updates_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('auto_updates.settings');
+    $last_check_timestamp = $this->checkerManager->getTimestamp();
+    $form['enable_readiness_checks'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Check the readiness of automatically updating the site.'),
+      '#default_value' => $config->get('enable_readiness_checks'),
+    ];
+    if ($this->checkerManager->isEnabled()) {
+      $readiness_messages = $last_check_timestamp === NULL ?
+        $this->t('Readiness checks have never been run.')
+        : $this->t('Readiness checks were last run @time ago.', ['@time' => $this->dateFormatter->formatTimeDiffSince($last_check_timestamp)]);
+      $form['enable_readiness_checks']['#description'] = $readiness_messages . ' ' . $this->t('Manually <a href=":url">run the readiness checks</a>.', [
+        ':url' => Url::fromRoute('auto_updates.update_readiness')->toString(),
+      ]);
+    }
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+    $form_state->cleanValues();
+    $config = $this->config('auto_updates.settings');
+    foreach ($form_state->getValues() as $key => $value) {
+      $config->set($key, $value);
+    }
+    $config->save();
+  }
+
+}
diff --git a/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php b/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php
new file mode 100644
index 0000000000..ea5a25c831
--- /dev/null
+++ b/core/modules/auto_updates/src/ReadinessChecker/DiskSpace.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\auto_updates\ReadinessChecker;
+
+use Drupal\Component\FileSystem\FileSystem as FileSystemComponent;
+
+/**
+ * A readiness checker that ensures there is enough disk space for updates.
+ */
+class DiskSpace extends FileSystemBase {
+
+  /**
+   * Minimum disk space (in bytes) is 100 MB.
+   *
+   * @todo Determine how much the minimum should be now that we will be using
+   *   Composer in https://www.drupal.org/node/3166416.
+   */
+  const MINIMUM_DISK_SPACE = 100000000;
+
+  /**
+   * Megabyte divisor.
+   */
+  const MEGABYTE_DIVISOR = 1000000;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doCheck(): array {
+    $messages = [];
+    $minimum_megabytes = static::MINIMUM_DISK_SPACE / static::MEGABYTE_DIVISOR;
+    if (!$this->areSameLogicalDisk($this->getRootPath(), $this->getVendorPath())) {
+      if (disk_free_space($this->getRootPath()) < static::MINIMUM_DISK_SPACE) {
+        $messages[] = $this->t('Drupal root filesystem "@root" has insufficient space. There must be at least @space megabytes free.', [
+          '@root' => $this->getRootPath(),
+          '@space' => $minimum_megabytes,
+        ]);
+      }
+      if (disk_free_space($this->getVendorPath()) < static::MINIMUM_DISK_SPACE) {
+        $messages[] = $this->t('Vendor filesystem "@vendor" has insufficient space. There must be at least @space megabytes free.', [
+          '@vendor' => $this->getVendorPath(),
+          '@space' => $minimum_megabytes,
+        ]);
+      }
+    }
+    elseif (disk_free_space($this->getRootPath()) < static::MINIMUM_DISK_SPACE) {
+      $messages[] = $this->t('Logical disk "@root" has insufficient space. There must be at least @space megabytes free.', [
+        '@root' => $this->getRootPath(),
+        '@space' => $minimum_megabytes,
+      ]);
+    }
+    $temp = FileSystemComponent::getOsTemporaryDirectory();
+    if (disk_free_space($temp) < static::MINIMUM_DISK_SPACE) {
+      $messages[] = $this->t('Directory "@temp" has insufficient space. There must be at least @space megabytes free.', [
+        '@temp' => $temp,
+        '@space' => $minimum_megabytes,
+      ]);
+    }
+    return $messages;
+  }
+
+}
diff --git a/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php b/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php
new file mode 100644
index 0000000000..1a6fb8f1d5
--- /dev/null
+++ b/core/modules/auto_updates/src/ReadinessChecker/FileSystemBase.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Drupal\auto_updates\ReadinessChecker;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Base class for file system checkers.
+ */
+abstract class FileSystemBase implements ReadinessCheckerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The root file path.
+   *
+   * @var string
+   */
+  protected $rootPath;
+
+  /**
+   * FileSystemBase constructor.
+   *
+   * @param string $app_root
+   *   The app root.
+   */
+  public function __construct(string $app_root) {
+    $this->rootPath = $app_root;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function run(): array {
+    $messages = [];
+    if (!file_exists(implode(DIRECTORY_SEPARATOR, [$this->getRootPath(), 'core', 'core.api.php']))) {
+      $messages[] = $this->t('The web root could not be located.');
+    }
+    if (!file_exists(implode(DIRECTORY_SEPARATOR, [$this->getVendorPath(), 'autoload.php']))) {
+      $messages[] = $this->t('Vendor folder "@vendor" is not a valid directory. Alternate vendor folder locations are not currently supported.', [
+        '@vendor' => $this->getVendorPath(),
+      ]);
+    }
+    if ($messages) {
+      return $messages;
+    }
+    return $this->doCheck();
+  }
+
+  /**
+   * Performs checks.
+   *
+   * @return array
+   *   An array of translatable strings if any checks fail.
+   */
+  abstract protected function doCheck(): array;
+
+  /**
+   * Gets the root file path.
+   *
+   * @return string
+   *   The root file path.
+   */
+  protected function getRootPath(): string {
+    return $this->rootPath;
+  }
+
+  /**
+   * Get the vendor file path.
+   *
+   * @return string
+   *   The vendor file path.
+   */
+  protected function getVendorPath(): string {
+    // @todo Support finding the 'vendor' directory dynamically in
+    // https://www.drupal.org/node/3166435.
+    return $this->getRootPath() . DIRECTORY_SEPARATOR . 'vendor';
+  }
+
+  /**
+   * Determines if the root and vendor directories are on the same logical disk.
+   *
+   * @param string $root
+   *   Root file path.
+   * @param string $vendor
+   *   Vendor file path.
+   *
+   * @return bool
+   *   TRUE if they are on the same file system, FALSE otherwise.
+   */
+  protected function areSameLogicalDisk(string $root, string $vendor): bool {
+    $root_statistics = stat($root);
+    $vendor_statistics = stat($vendor);
+    return $root_statistics && $vendor_statistics && $root_statistics['dev'] === $vendor_statistics['dev'];
+  }
+
+}
diff --git a/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerInterface.php b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerInterface.php
new file mode 100644
index 0000000000..d370b1e9d8
--- /dev/null
+++ b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerInterface.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Drupal\auto_updates\ReadinessChecker;
+
+/**
+ * Defines an interface for readiness checker services.
+ */
+interface ReadinessCheckerInterface {
+
+  /**
+   * Runs a check.
+   *
+   * @return array
+   *   An array of translatable strings if any checks fail, otherwise an empty
+   *   array.
+   */
+  public function run(): array;
+
+}
diff --git a/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php
new file mode 100644
index 0000000000..f77076a6af
--- /dev/null
+++ b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManager.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Drupal\auto_updates\ReadinessChecker;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
+
+/**
+ * Defines a manager to run readiness checkers.
+ */
+class ReadinessCheckerManager implements ReadinessCheckerManagerInterface {
+
+  /**
+   * Time (in seconds) since the last check after which we generate a warning.
+   *
+   * Defaults to 1 day.
+   */
+  private const LAST_CHECKED_WARNING = 60 * 60 * 24;
+
+  /**
+   * The key/value storage.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $keyValue;
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * An unsorted array of active checkers.
+   *
+   * The keys are category, next level is integers that indicate priority.
+   * Values are arrays of ReadinessCheckerInterface objects.
+   *
+   * @var \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerInterface[][][]
+   */
+  protected $checkers = [];
+
+  /**
+   * The time service.
+   *
+   * @var \Drupal\Component\Datetime\TimeInterface
+   */
+  protected $time;
+
+  /**
+   * ReadinessCheckerManager constructor.
+   *
+   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
+   *   The key/value service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Component\Datetime\TimeInterface $time
+   *   The time service.
+   */
+  public function __construct(KeyValueFactoryInterface $key_value, ConfigFactoryInterface $config_factory, TimeInterface $time) {
+    $this->keyValue = $key_value->get('auto_updates');
+    $this->configFactory = $config_factory;
+    $this->time = $time;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChecker(ReadinessCheckerInterface $checker, string $category = 'warning', $priority = 0): ReadinessCheckerManagerInterface {
+    if (!in_array($category, $this->getCategories(), TRUE)) {
+      throw new \InvalidArgumentException(sprintf('Readiness checker category "%s" is invalid. Use "%s" instead.', $category, implode('" or "', $this->getCategories())));
+    }
+    $this->checkers[$category][$priority][] = $checker;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function run(): array {
+    if (!$this->isEnabled()) {
+      return [];
+    }
+    $messages_by_category = [];
+    $sorted_checkers = $this->getSortedCheckers();
+    foreach ($sorted_checkers as $category => $checkers) {
+      foreach ($checkers as $checker) {
+        if ($messages = $checker->run()) {
+          $messages_by_category[$category] = array_merge($messages_by_category, $messages);
+        }
+      }
+    }
+
+    $this->keyValue->set('readiness_check_results',
+      [
+        'messages' => $messages_by_category,
+        'checkers' => $this->getCurrentCheckerIds(),
+      ]
+    );
+    $this->keyValue->set('readiness_check_timestamp', $this->time->getRequestTime());
+    return $messages_by_category;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getResults($category): array {
+    if ($this->isEnabled()) {
+      $results = $this->keyValue->get('readiness_check_results', []);
+      $all_messages = $results['messages'] ?? [];
+      return $all_messages[$category] ?? [];
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearStaleResults(): bool {
+    $results = $this->keyValue->get('readiness_check_results');
+    if (isset($results['checkers']) && $this->getCurrentCheckerIds() !== $results['checkers']) {
+      $this->keyValue->delete('readiness_check_results');
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTimestamp(): int {
+    return $this->keyValue->get('readiness_check_timestamp');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEnabled(): bool {
+    return $this->configFactory->get('auto_updates.settings')->get('enable_readiness_checks');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategories(): array {
+    return [self::ERROR, self::WARNING];
+  }
+
+  /**
+   * Sorts checkers according to priority.
+   *
+   * @return \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerInterface[][]
+   *   A nested and sorted array of checker objects. The first level of the
+   *   array is keyed by checker categories. The second level array is checker
+   *   objects in the category ordered by priority.
+   */
+  protected function getSortedCheckers(): array {
+    $sorted = [];
+    foreach ($this->checkers as $category => $priorities) {
+      foreach ($priorities as $checkers) {
+        krsort($checkers);
+        $sorted[$category][] = $checkers;
+      }
+      $sorted[$category] = array_merge(...$sorted[$category]);
+    }
+    return $sorted;
+  }
+
+  /**
+   * Gets the current checker service Ids.
+   *
+   * @return string
+   *   A concatenated list of checker service Ids delimited by '::'.
+   */
+  protected function getCurrentCheckerIds(): string {
+    $service_ids = [];
+    foreach ($this->getSortedCheckers() as $category => $checkers) {
+      foreach ($checkers as $checker) {
+        $service_ids[] = $checker->_serviceId;
+      }
+    }
+    return implode('::', $service_ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasRunRecently(): bool {
+    return $this->time->getRequestTime() <= $this->getTimestamp() + self::LAST_CHECKED_WARNING;
+  }
+
+}
diff --git a/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
new file mode 100644
index 0000000000..0280b6663e
--- /dev/null
+++ b/core/modules/auto_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\auto_updates\ReadinessChecker;
+
+/**
+ * Readiness checker manager interface.
+ */
+interface ReadinessCheckerManagerInterface {
+
+  /**
+   * Error category.
+   */
+  const ERROR = 'error';
+
+  /**
+   * Warning category.
+   */
+  const WARNING = 'warning';
+
+  /**
+   * Appends a checker to the checker chain.
+   *
+   * @param \Drupal\auto_updates\ReadinessChecker\ReadinessCheckerInterface $checker
+   *   The checker interface to be appended to the checker chain.
+   * @param string $category
+   *   (optional) The category of check. Defaults to 'warning'
+   * @param int $priority
+   *   (optional) The priority of the checker being added. Defaults to 0.
+   *   Readiness checkers with larger priorities will run first within a
+   *   category.
+   *
+   * @return $this
+   */
+  public function addChecker(ReadinessCheckerInterface $checker, string $category = 'warning', $priority = 0): ReadinessCheckerManagerInterface;
+
+  /**
+   * Runs readiness checks.
+   *
+   * @return string[][]
+   *   A nested array of readiness check messages. The top level array is keyed
+   *   by category and the next level array is an array of translatable strings
+   *   for the category.
+   */
+  public function run(): array;
+
+  /**
+   * Gets the results of the most recent run.
+   *
+   * @param string $category
+   *   The category of check.
+   *
+   * @return array
+   *   An array of translatable messages if any checks fail, otherwise an empty
+   *   array.
+   */
+  public function getResults($category): array;
+
+  /**
+   * Gets the timestamp of the most recent run.
+   *
+   * @return int|null
+   *   The timestamp of the most recently completed run, or NULL if no run has
+   *   been completed.
+   */
+  public function getTimestamp();
+
+  /**
+   * Determines if readiness checks are enabled.
+   *
+   * @return bool
+   *   TRUE if enabled, otherwise FALSE.
+   */
+  public function isEnabled(): bool;
+
+  /**
+   * Gets the checker categories.
+   *
+   * @return string[]
+   *   The checkers categories.
+   */
+  public function getCategories(): array;
+
+  /**
+   * Clears readiness checker results if the available checkers have changed.
+   *
+   * @return bool
+   *   TRUE if the results were cleared, otherwise FALSE.
+   */
+  public function clearStaleResults(): bool;
+
+  /**
+   * Determines whether the readiness checkers have been run recently.
+   *
+   * @return bool
+   *   TRUE if the checkers have been run recently, otherwise FALSE.
+   */
+  public function hasRunRecently(): bool;
+
+}
diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.info.yml b/core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.info.yml
similarity index 35%
copy from core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.info.yml
copy to core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.info.yml
index 6cd09f9f1d..b411b31823 100644
--- a/core/modules/big_pipe/tests/modules/big_pipe_test/big_pipe_test.info.yml
+++ b/core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.info.yml
@@ -1,5 +1,5 @@
-name: 'BigPipe test'
+name: 'Automatic Updates Test'
 type: module
-description: 'Support module for BigPipe testing.'
+description: 'Module for testing Automatic Updates.'
 package: Testing
 version: VERSION
diff --git a/core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.services.yml b/core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.services.yml
new file mode 100644
index 0000000000..12de8a27ba
--- /dev/null
+++ b/core/modules/auto_updates/tests/modules/auto_updates_test/auto_updates_test.services.yml
@@ -0,0 +1,8 @@
+services:
+  auto_updates_test.checker:
+    class: Drupal\auto_updates_test\ReadinessChecker\TestChecker
+    tags:
+      - { name: readiness_checker, category: error}
+  datetime.time:
+    class: Drupal\auto_updates_test\Datetime\TestTime
+    arguments: ['@request_stack']
diff --git a/core/modules/update/tests/modules/update_test/src/Datetime/TestTime.php b/core/modules/auto_updates/tests/modules/auto_updates_test/src/Datetime/TestTime.php
similarity index 46%
copy from core/modules/update/tests/modules/update_test/src/Datetime/TestTime.php
copy to core/modules/auto_updates/tests/modules/auto_updates_test/src/Datetime/TestTime.php
index de9e5b86e4..3bbc9d606a 100644
--- a/core/modules/update/tests/modules/update_test/src/Datetime/TestTime.php
+++ b/core/modules/auto_updates/tests/modules/auto_updates_test/src/Datetime/TestTime.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Drupal\update_test\Datetime;
+namespace Drupal\auto_updates_test\Datetime;
 
 use Drupal\Component\Datetime\Time;
 
@@ -9,12 +9,16 @@
  */
 class TestTime extends Time {
 
+  const TIME_FORMAT = 'U';
+
+  const STATE_KEY = 'auto_updates_test.mock_date_time';
+
   /**
    * {@inheritdoc}
    */
   public function getRequestTime() {
-    if ($mock_date = \Drupal::state()->get('update_test.mock_date', NULL)) {
-      return \DateTime::createFromFormat('Y-m-d', $mock_date)->getTimestamp();
+    if ($mock_date = \Drupal::state()->get(TestTime::STATE_KEY, NULL)) {
+      return \DateTime::createFromFormat(self::TIME_FORMAT, $mock_date)->getTimestamp();
     }
     return parent::getRequestTime();
   }
diff --git a/core/modules/auto_updates/tests/modules/auto_updates_test/src/ReadinessChecker/TestChecker.php b/core/modules/auto_updates/tests/modules/auto_updates_test/src/ReadinessChecker/TestChecker.php
new file mode 100644
index 0000000000..1641cd468f
--- /dev/null
+++ b/core/modules/auto_updates/tests/modules/auto_updates_test/src/ReadinessChecker/TestChecker.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\auto_updates_test\ReadinessChecker;
+
+use Drupal\auto_updates\ReadinessChecker\ReadinessCheckerInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * The disk space readiness checker.
+ */
+class TestChecker implements ReadinessCheckerInterface {
+
+  use StringTranslationTrait;
+
+  const STATE_KEY = 'auto_updates_test.check_error';
+
+  /**
+   * {@inheritDoc}
+   */
+  public function run(): array {
+    if ($message = \Drupal::state()->get(static::STATE_KEY, NULL)) {
+      return [$message];
+    }
+    return [];
+  }
+
+}
diff --git a/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php
new file mode 100644
index 0000000000..44a0e09c6e
--- /dev/null
+++ b/core/modules/auto_updates/tests/src/Functional/ReadinessCheckerTest.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace Drupal\Tests\auto_updates\Functional;
+
+use Drupal\auto_updates_test\Datetime\TestTime;
+use Drupal\auto_updates_test\ReadinessChecker\TestChecker;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests readiness checkers.
+ *
+ * @group auto_updates
+ */
+class ReadinessCheckerTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * A user who can view the status report.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $reportViewerUser;
+
+  /**
+   * A user how can view the status report and run readiness checkers.
+   *
+   * @var \Drupal\user\Entity\User
+   */
+  protected $checkerRunnerUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->reportViewerUser = $this->createUser([
+      'administer site configuration',
+    ]);
+    $this->checkerRunnerUser = $this->createUser([
+      'administer site configuration',
+      'administer software updates',
+    ]);
+  }
+
+  /**
+   * Tests readiness checkers on status report page.
+   */
+  public function testReadinessChecksStatusReport() {
+    $assert = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Disable automated_cron before installing auto_updates. This ensures we
+    // are testing that auto_updates runs the checkers when the module itself
+    // is installed and they weren't run on cron.
+    $this->container->get('module_installer')->uninstall(['automated_cron']);
+    $this->container->get('module_installer')->install(['auto_updates', 'auto_updates_test']);
+
+    // If the site is ready for updates, the users will see the same output
+    // regardless of whether the user has permission to run updates.
+    $this->drupalLogin($this->reportViewerUser);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site is ready for automatic updates.');
+    $this->drupalLogin($this->checkerRunnerUser);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site is ready for automatic updates.');
+
+    // Confirm a user without the permission to run readiness checks does not
+    // have a link to run the checks when the checks need to be run again.
+    $this->setFakeTime('+2 days');
+    $this->drupalLogin($this->reportViewerUser);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site has not recently checked if it is ready to apply automatic updates. Readiness checks were last run %s ago.');
+
+    // Confirm a user with the permission to run readiness checks does have a
+    // link to run the checks when the checks need to be run again.
+    $this->drupalLogin($this->checkerRunnerUser);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site has not recently checked if it is ready to apply automatic updates.'
+      . ' Readiness checks were last run %s ago. Run readiness checks manually.');
+    $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 🚒. Your server is on 🔥!');
+
+    // Run the readiness checks.
+    $this->clickLink('Run readiness checks');
+    // @todo If coming from the status report page should you be redirected there?
+    //   This is how 'Run cron' works.
+    $assert->statusCodeEquals(200);
+    $assert->addressEquals('/admin/config/auto_updates');
+    $assert->checkboxChecked('enable_readiness_checks');
+    $assert->pageTextNotContains('Access denied');
+    $assert->pageTextContains('Your site is currently failing readiness checks for automatic updates. It cannot be automatically updated until further action is performed.');
+    $assert->pageTextContains('OMG 🚒. Your server is on 🔥!');
+
+    // Confirm the error is displayed on the status report page.
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!');
+    // @todo Should we always show when the checks were last run and a link to
+    //   run when there is an error?
+    // Confirm a user without permission to run the checks sees the same error.
+    $this->drupalLogin($this->reportViewerUser);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!');
+
+    // Disable readiness checks.
+    $this->drupalLogin($this->checkerRunnerUser);
+    $this->drupalGet('admin/config/auto_updates');
+    $page->uncheckField('enable_readiness_checks');
+    $page->pressButton('Save configuration');
+
+    // Confirm that when readiness checkers are disabled no information on the
+    // last run is displayed.
+    $assert->pageTextNotContains('Readiness checks have never been run.');
+    $assert->pageTextNotContains('Readiness checks were last run');
+    $assert->pageTextNotContains('run the readiness checks');
+
+    // Confirm that access is denied when manually going to the readiness
+    // checker controller.
+    $this->drupalGet('admin/config/auto_updates/readiness');
+    $assert->statusCodeEquals(403);
+    $assert->pageTextContains('Access denied');
+
+    $this->drupalGet('admin/reports/status');
+    $assert->pageTextNotContains('Update readiness checks');
+
+    // Re-enable readiness checks.
+    $this->drupalGet('admin/config/auto_updates');
+    $page->checkField('enable_readiness_checks');
+    $page->pressButton('Save configuration');
+
+    // Confirm that the last message displayed is displayed again.
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!');
+
+  }
+
+  /**
+   * Tests installing a module with a checker before installing auto_updates.
+   */
+  public function testReadinessCheckAfterInstall() {
+    $assert = $this->assertSession();
+    $this->drupalLogin($this->checkerRunnerUser);
+
+    $this->drupalGet('admin/reports/status');
+    $assert->pageTextNotContains('Update readiness checks');
+
+    $this->container->get('module_installer')->install(['auto_updates']);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site is ready for automatic updates.');
+
+    $this->container->get('state')->set(TestChecker::STATE_KEY, '😿Oh no! A hacker now owns your files!');
+    $this->container->get('module_installer')->install(['auto_updates_test']);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: 😿Oh no! A hacker now owns your files!');
+
+    // Confirm that installing a module that does not provide a new checker does
+    // not run the checkers on install.
+    $this->container->get('state')->set(TestChecker::STATE_KEY, 'Security has been compromised. "pass123" was a bad password!');
+    $this->container->get('module_installer')->install(['help']);
+    $this->drupalGet('admin/reports/status');
+    // Confirm that new checker message is not displayed because the checker was
+    // not run again.
+    $this->assertReadinessReportMatches('1 check failed: 😿Oh no! A hacker now owns your files!');
+
+    // Confirm the new message is displayed after running the checkers manually.
+    $this->drupalGet('admin/config/auto_updates');
+    $this->clickLink('run the readiness checks');
+    $assert->pageTextContains('Security has been compromised. "pass123" was a bad password!');
+    $assert->pageTextNotContains('😿Oh no! A hacker now owns your files!');
+
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: Security has been compromised. "pass123" was a bad password!');
+  }
+
+  /**
+   * Tests that the readiness checks are run on cron.
+   */
+  public function testCronRun() {
+    $assert = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalLogin($this->reportViewerUser);
+    $this->container->get('module_installer')->install(['auto_updates', 'auto_updates_test']);
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('Your site is ready for automatic updates.');
+
+    $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 🚒. Your server is on 🔥!');
+
+    // Tests that running cron within 1 hour of the checkers running will not
+    // run them again.
+    $this->setFakeTime('+30 minutes');
+    $this->clickLink('Run cron');
+    $this->assertReadinessReportMatches('Your site is ready for automatic updates.');
+
+    // Tests that running cron after 1 hour of the checkers running will run
+    // them again.
+    $this->setFakeTime('+65 minutes');
+    $this->clickLink('Run cron');
+    $this->assertReadinessReportMatches('1 check failed: OMG 🚒. Your server is on 🔥!');
+
+    // Tests that running cron after 1 hour of the checkers running during cron
+    // will run them again.
+    $this->container->get('state')->set(TestChecker::STATE_KEY, 'OMG 💦. Now your server is filled with water!');
+    $this->setFakeTime('+125 minutes');
+    $this->clickLink('Run cron');
+    $this->assertReadinessReportMatches('1 check failed: OMG 💦. Now your server is filled with water!');
+
+    // Disable readiness checks.
+    $this->drupalLogin($this->checkerRunnerUser);
+    $this->drupalGet('admin/config/auto_updates');
+    $page->uncheckField('enable_readiness_checks');
+    $page->pressButton('Save configuration');
+
+    // Run cron while readiness checks are disabled.
+    $this->container->get('state')->set(TestChecker::STATE_KEY, '😨 Now your hard drive is missing! How is that even possible?');
+    $this->setFakeTime('+190 minutes');
+    $this->drupalGet('admin/reports/status');
+    $assert->pageTextNotContains('Update readiness checks');
+    $this->clickLink('Run cron');
+    $assert->pageTextNotContains('Update readiness checks');
+
+    // Re-enable readiness checks.
+    $this->drupalGet('admin/config/auto_updates');
+    $page->checkField('enable_readiness_checks');
+    $page->pressButton('Save configuration');
+
+    // Confirm that the new test message for the test readiness checker is not
+    // displayed because the checkers were not run during cron when they were
+    // disabled.
+    $this->drupalGet('admin/reports/status');
+    $this->assertReadinessReportMatches('1 check failed: OMG 💦. Now your server is filled with water!');
+
+    // Confirm running cron displays the new message.
+    $this->setFakeTime('+255 minutes');
+    $this->clickLink('Run cron');
+    $this->assertReadinessReportMatches('1 check failed: 😨 Now your hard drive is missing! How is that even possible?');
+  }
+
+  /**
+   * Sets a fake time that will be used in the test.
+   *
+   * @param string $offset
+   *   A date/time offset string.
+   */
+  private function setFakeTime(string $offset): void {
+    $fake_delay = (new \DateTime())->modify($offset)->format(TestTime::TIME_FORMAT);
+    $this->container->get('state')->set(TestTime::STATE_KEY, $fake_delay);
+  }
+
+  /**
+   * Asserts status report readiness report item matches a format.
+   *
+   * @param string $format
+   *   The string to match.
+   */
+  private function assertReadinessReportMatches(string $format): void {
+    // Prefix the expected format with the item title which does not change.
+    $format = "Update readiness checks $format";
+    $text = $this->getSession()->getPage()->find(
+      'css',
+      'details.system-status-report__entry:contains("Update readiness checks")'
+    )->getText();
+    $this->assertStringMatchesFormat($format, $text);
+  }
+
+}
diff --git a/core/modules/auto_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php b/core/modules/auto_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php
new file mode 100644
index 0000000000..b4d0264e0a
--- /dev/null
+++ b/core/modules/auto_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\Tests\auto_updates\Kernel\ReadinessChecker;
+
+use Drupal\auto_updates\ReadinessChecker\DiskSpace;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests disk space readiness checking.
+ *
+ * @group auto_updates
+ */
+class DiskSpaceTest extends KernelTestBase {
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['auto_updates'];
+
+  /**
+   * Tests the functionality of disk space readiness checks.
+   */
+  public function testDiskSpace() {
+    // No disk space issues.
+    $disk_space = new DiskSpace($this->container->getParameter('app.root'));
+    $messages = $disk_space->run();
+    $this->assertEmpty($messages);
+
+    // Out of space.
+    $disk_space = new TestDiskSpace($this->container->getParameter('app.root'));
+    $messages = $disk_space->run();
+    $this->assertCount(2, $messages);
+    $this->assertStringMatchesFormat('Logical disk "%s" has insufficient space. There must be at least %s megabytes free.', (string) $messages[0]);
+    $this->assertStringMatchesFormat('Directory "%s" has insufficient space. There must be at least %s megabytes free.', (string) $messages[1]);
+
+    // Out of space not the same logical disk.
+    $disk_space = new TestDiskSpaceNonSameDisk($this->container->getParameter('app.root'));
+    $messages = $disk_space->run();
+    $this->assertCount(3, $messages);
+    $this->assertStringMatchesFormat('Drupal root filesystem "%s" has insufficient space. There must be at least %s megabytes free.', (string) $messages[0]);
+    $this->assertStringMatchesFormat('Vendor filesystem "%s" has insufficient space. There must be at least %s megabytes free.', (string) $messages[1]);
+    $this->assertStringMatchesFormat('Directory "%s" has insufficient space. There must be at least %s megabytes free.', (string) $messages[2]);
+
+    // Web root and vendor path are invalid.
+    $disk_space = new DiskSpace("if_there_was_ever_a_folder_with_this_path_this_test_would_fail");
+    $messages = $disk_space->run();
+    $this->assertCount(2, $messages);
+    $this->assertEquals('The web root could not be located.', (string) $messages[0]);
+    $this->assertStringMatchesFormat('Vendor folder "if_there_was_ever_a_folder_with_this_path_this_test_would_fail/vendor" is not a valid directory. Alternate vendor folder locations are not currently supported.', (string) $messages[1]);
+  }
+
+}
+
+/**
+ * Test checker with the free disk space minimum set to a very high number.
+ */
+class TestDiskSpace extends DiskSpace {
+
+  /**
+   * {@inheritdoc}
+   */
+  const MINIMUM_DISK_SPACE = 99999999999999999999999999999999999999999999999999;
+
+}
+
+/**
+ * Class TestDiskSpaceNonSameDisk.
+ */
+class TestDiskSpaceNonSameDisk extends TestDiskSpace {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function areSameLogicalDisk(string $root, string $vendor): bool {
+    return FALSE;
+  }
+
+}
