diff --git a/core/modules/automatic_updates/automatic_updates.info.yml b/core/modules/automatic_updates/automatic_updates.info.yml
new file mode 100644
index 0000000000..889bc95fbc
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.info.yml
@@ -0,0 +1,6 @@
+name: 'Automatic Updates'
+type: module
+description: 'Experimental module to develop automatic updates. Currently the module does not provide update functionality.'
+package: Core (Experimental)
+version: VERSION
+hidden: true
diff --git a/core/modules/automatic_updates/automatic_updates.install b/core/modules/automatic_updates/automatic_updates.install
new file mode 100644
index 0000000000..c16b2fb3c0
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.install
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Automatic Updates.
+ */
+
+use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_requirements().
+ */
+function automatic_updates_requirements($phase) {
+  if ($phase !== 'runtime') {
+    return NULL;
+  }
+
+  $requirements = [];
+  _automatic_updates_checker_requirements($requirements);
+  return $requirements;
+}
+
+/**
+ * Display requirements from results of readiness checker.
+ *
+ * @param array $requirements
+ *   The requirements array.
+ */
+function _automatic_updates_checker_requirements(array &$requirements) {
+  /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
+  $checker = \Drupal::service('automatic_updates.readiness_checker');
+  if (!$checker->isEnabled()) {
+    return;
+  }
+
+  $last_check_timestamp = $checker->timestamp();
+  $requirements['automatic_updates_readiness'] = [
+    'title' => t('Update readiness checks'),
+    'severity' => REQUIREMENT_OK,
+    'value' => t('Your site is ready to for <a href="@readiness_checks">automatic updates</a>.', ['@readiness_checks' => 'https://www.drupal.org/docs/8/update/automatic-updates#readiness-checks']),
+  ];
+  $error_results = $checker->getResults(ReadinessCheckerManagerInterface::ERROR);
+  $warning_results = $checker->getResults(ReadinessCheckerManagerInterface::WARNING);
+  $checker_results = array_merge($error_results, $warning_results);
+  if (!empty($checker_results)) {
+    $requirements['automatic_updates_readiness']['severity'] = $error_results ? REQUIREMENT_ERROR : REQUIREMENT_WARNING;
+    $requirements['automatic_updates_readiness']['value'] = new PluralTranslatableMarkup(count($checker_results), '@count check failed:', '@count checks failed:');
+    $requirements['automatic_updates_readiness']['description'] = [
+      '#theme' => 'item_list',
+      '#items' => $checker_results,
+    ];
+  }
+  if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
+    $requirements['automatic_updates_readiness']['severity'] = REQUIREMENT_ERROR;
+    $requirements['automatic_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']);
+    $readiness_check = Url::fromRoute('automatic_updates.update_readiness');
+    $time_ago = \Drupal::service('date.formatter')->formatTimeDiffSince($last_check_timestamp);
+    if ($last_check_timestamp === 0) {
+      $requirements['automatic_updates_readiness']['description'] = t('<a href="@link">Run readiness checks</a> manually.', [
+        '@link' => $readiness_check->toString(),
+      ]);
+    }
+    elseif ($readiness_check->access()) {
+      $requirements['automatic_updates_readiness']['description'] = t('Last run @time ago. <a href="@link">Run readiness checks</a> manually.', [
+        '@time' => $time_ago,
+        '@link' => $readiness_check->toString(),
+      ]);
+    }
+    else {
+      $requirements['automatic_updates_readiness']['description'] = t('Readiness checks were last run @time ago.', ['@time' => $time_ago]);
+    }
+  }
+}
diff --git a/core/modules/automatic_updates/automatic_updates.links.menu.yml b/core/modules/automatic_updates/automatic_updates.links.menu.yml
new file mode 100644
index 0000000000..a2d959f9a8
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.links.menu.yml
@@ -0,0 +1,5 @@
+automatic_updates.settings:
+  title: 'Automatic updates'
+  route_name: automatic_updates.settings
+  description: 'Configure automatic update settings.'
+  parent: system.admin_config_system
diff --git a/core/modules/automatic_updates/automatic_updates.module b/core/modules/automatic_updates/automatic_updates.module
new file mode 100644
index 0000000000..12574bf49f
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.module
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Automatic Updates.
+ */
+
+use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\Core\Url;
+/**
+ * Implements hook_page_top().
+ */
+function automatic_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;
+    }
+    $last_check_timestamp = \Drupal::service('automatic_updates.readiness_checker')->timestamp();
+    if (\Drupal::time()->getRequestTime() > $last_check_timestamp + ReadinessCheckerManagerInterface::LAST_CHECKED_WARNING) {
+      $readiness_settings = Url::fromRoute('automatic_updates.settings');
+      \Drupal::messenger()->addError(t('Your site has not recently run an update readiness check. <a href="@link">Administer automatic updates</a> and run readiness checks manually.', [
+        '@link' => $readiness_settings->toString(),
+      ]));
+    }
+    /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker */
+    $checker = \Drupal::service('automatic_updates.readiness_checker');
+    $results = $checker->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/automatic-updates#readiness-checks']));
+      foreach ($results as $message) {
+        \Drupal::messenger()->addError($message);
+      }
+    }
+    $results = $checker->getResults('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/automatic-updates#readiness-checks']));
+      foreach ($results as $message) {
+        \Drupal::messenger()->addWarning($message);
+      }
+    }
+  }
+}
diff --git a/core/modules/automatic_updates/automatic_updates.routing.yml b/core/modules/automatic_updates/automatic_updates.routing.yml
new file mode 100644
index 0000000000..e831b37ca6
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.routing.yml
@@ -0,0 +1,18 @@
+automatic_updates.settings:
+  path: '/admin/config/automatic_updates'
+  defaults:
+    _form: '\Drupal\automatic_updates\Form\SettingsForm'
+    _title: 'Automatic Updates'
+  requirements:
+    _permission: 'administer software updates'
+  options:
+    _admin_route: TRUE
+automatic_updates.update_readiness:
+  path: '/admin/config/automatic_updates/readiness'
+  defaults:
+    _controller: '\Drupal\automatic_updates\Controller\ReadinessCheckerController::run'
+    _title: 'Update readiness checking...'
+  requirements:
+    _permission: 'administer software updates'
+  options:
+    _admin_route: TRUE
diff --git a/core/modules/automatic_updates/automatic_updates.services.yml b/core/modules/automatic_updates/automatic_updates.services.yml
new file mode 100644
index 0000000000..0e902f8506
--- /dev/null
+++ b/core/modules/automatic_updates/automatic_updates.services.yml
@@ -0,0 +1,14 @@
+services:
+  logger.channel.automatic_updates:
+    parent: logger.channel_base
+    arguments: ['automatic_updates']
+  automatic_updates.disk_space_checker:
+    class: Drupal\automatic_updates\ReadinessChecker\DiskSpace
+    arguments: ['%app.root%']
+    tags:
+      - { name: readiness_checker, category: error}
+  automatic_updates.readiness_checker:
+    class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager
+    arguments: ['@keyvalue', '@config.factory']
+    tags:
+      - { name: service_collector, tag: readiness_checker, call: addChecker }
diff --git a/core/modules/automatic_updates/config/install/automatic_updates.settings.yml b/core/modules/automatic_updates/config/install/automatic_updates.settings.yml
new file mode 100644
index 0000000000..de0b8d7d73
--- /dev/null
+++ b/core/modules/automatic_updates/config/install/automatic_updates.settings.yml
@@ -0,0 +1 @@
+enable_readiness_checks: true
diff --git a/core/modules/automatic_updates/config/schema/automatic_updates.schema.yml b/core/modules/automatic_updates/config/schema/automatic_updates.schema.yml
new file mode 100644
index 0000000000..156844881a
--- /dev/null
+++ b/core/modules/automatic_updates/config/schema/automatic_updates.schema.yml
@@ -0,0 +1,7 @@
+automatic_updates.settings:
+  type: config_object
+  label: 'Automatic updates settings'
+  mapping:
+    enable_readiness_checks:
+      type: boolean
+      label: 'Enable readiness checks'
diff --git a/core/modules/automatic_updates/src/Controller/ReadinessCheckerController.php b/core/modules/automatic_updates/src/Controller/ReadinessCheckerController.php
new file mode 100644
index 0000000000..0976e7bb80
--- /dev/null
+++ b/core/modules/automatic_updates/src/Controller/ReadinessCheckerController.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace Drupal\automatic_updates\Controller;
+
+use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\StringTranslation\TranslationInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A controller for running Readiness Checkers.
+ */
+class ReadinessCheckerController extends ControllerBase {
+
+  /**
+   * The readiness checker.
+   *
+   * @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface
+   */
+  protected $checker;
+
+  /**
+   * ReadinessCheckerController constructor.
+   *
+   * @param \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $checker
+   *   The readiness checker manager.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(ReadinessCheckerManagerInterface $checker, TranslationInterface $string_translation) {
+    $this->checker = $checker;
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('automatic_updates.readiness_checker'),
+      $container->get('string_translation')
+    );
+  }
+
+  /**
+   * Run the readiness checkers.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   A redirect response object.
+   */
+  public function run() {
+    $messages = [];
+    foreach ($this->checker->getCategories() as $category) {
+      $messages[] = $this->checker->run($category);
+    }
+    $messages = array_merge(...$messages);
+    if (empty($messages)) {
+      $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/automatic-updates#readiness-checks']));
+    }
+    return $this->redirect('automatic_updates.settings');
+  }
+
+}
diff --git a/core/modules/automatic_updates/src/Form/SettingsForm.php b/core/modules/automatic_updates/src/Form/SettingsForm.php
new file mode 100644
index 0000000000..dc3e8bab83
--- /dev/null
+++ b/core/modules/automatic_updates/src/Form/SettingsForm.php
@@ -0,0 +1,130 @@
+<?php
+
+namespace Drupal\automatic_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\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface
+   */
+  protected $checker;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Drupal root path.
+   *
+   * @var string
+   */
+  protected $drupalRoot;
+
+  /**
+   * The update manager service.
+   *
+   * @var \Drupal\update\UpdateManagerInterface
+   */
+  protected $updateManager;
+
+  /**
+   * The update processor.
+   *
+   * @var \Drupal\update\UpdateProcessorInterface
+   */
+  protected $updateProcessor;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $instance = parent::create($container);
+    $instance->checker = $container->get('automatic_updates.readiness_checker');
+    $instance->dateFormatter = $container->get('date.formatter');
+    $instance->drupalRoot = (string) $container->get('app.root');
+    $instance->updateManager = $container->get('update.manager');
+    $instance->updateProcessor = $container->get('update.processor');
+    return $instance;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'automatic_updates.settings',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'automatic_updates_settings_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('automatic_updates.settings');
+
+    $form['readiness'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Readiness checks'),
+      '#open' => TRUE,
+    ];
+
+    $last_check_timestamp = $this->checker->timestamp();
+    $form['readiness']['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->checker->isEnabled()) {
+      $form['readiness']['enable_readiness_checks']['#description'] = $this->t('Readiness checks were last run @time ago. Manually <a href="@link">run the readiness checks</a>.', [
+        '@time' => $this->dateFormatter->formatTimeDiffSince($last_check_timestamp),
+        '@link' => Url::fromRoute('automatic_updates.update_readiness')->toString(),
+      ]);
+    }
+    $form['readiness']['ignored_paths'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Paths to ignore for readiness checks'),
+      '#description' => $this->t('Paths relative to %drupal_root. One path per line. Automatic Updates is intentionally limited to Drupal core. It is recommended to ignore paths to contrib extensions.', ['%drupal_root' => $this->drupalRoot]),
+      '#default_value' => $config->get('ignored_paths'),
+      '#states' => [
+        'visible' => [
+          ':input[name="enable_readiness_checks"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
+    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('automatic_updates.settings');
+    foreach ($form_state->getValues() as $key => $value) {
+      $config->set($key, $value);
+    }
+    $config->save();
+  }
+
+}
diff --git a/core/modules/automatic_updates/src/ReadinessChecker/DiskSpace.php b/core/modules/automatic_updates/src/ReadinessChecker/DiskSpace.php
new file mode 100644
index 0000000000..82d86eef5e
--- /dev/null
+++ b/core/modules/automatic_updates/src/ReadinessChecker/DiskSpace.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\automatic_updates\ReadinessChecker;
+
+use Drupal\Component\FileSystem\FileSystem as FileSystemComponent;
+
+/**
+ * The disk space readiness checker.
+ */
+class DiskSpace extends Filesystem {
+
+  /**
+   * Minimum disk space (in bytes) is 10mb.
+   */
+  const MINIMUM_DISK_SPACE = 10000000;
+
+  /**
+   * Megabyte divisor.
+   */
+  const MEGABYTE_DIVISOR = 1000000;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function doCheck() {
+    return $this->diskSpaceCheck();
+  }
+
+  /**
+   * Check if the filesystem has sufficient disk space.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
+   *   An array of translatable strings if there is not sufficient space.
+   */
+  protected function diskSpaceCheck() {
+    $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 (is_dir($this->getVendorPath()) && 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/automatic_updates/src/ReadinessChecker/Filesystem.php b/core/modules/automatic_updates/src/ReadinessChecker/Filesystem.php
new file mode 100644
index 0000000000..48eb4b87c8
--- /dev/null
+++ b/core/modules/automatic_updates/src/ReadinessChecker/Filesystem.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace Drupal\automatic_updates\ReadinessChecker;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Base class for filesystem checkers.
+ */
+abstract class Filesystem implements ReadinessCheckerInterface {
+  use StringTranslationTrait;
+
+  /**
+   * The root file path.
+   *
+   * @var string
+   */
+  protected $rootPath;
+
+  /**
+   * The vendor file path.
+   *
+   * @var string
+   */
+  protected $vendorPath;
+
+  /**
+   * Filesystem constructor.
+   *
+   * @param string $app_root
+   *   The app root.
+   */
+  public function __construct(string $app_root) {
+    $this->rootPath = $app_root;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function run() {
+    if (!file_exists($this->getRootPath() . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, ['core', 'core.api.php']))) {
+      return [$this->t('The web root could not be located.')];
+    }
+
+    return $this->doCheck();
+  }
+
+  /**
+   * Perform checks.
+   *
+   * @return array
+   *   An array of translatable strings if any checks fail.
+   */
+  abstract protected function doCheck();
+
+  /**
+   * Get the root file path.
+   *
+   * @return string
+   *   The root file path.
+   */
+  protected function getRootPath() {
+    if (!$this->rootPath) {
+      $this->rootPath = (string) \Drupal::root();
+    }
+    return $this->rootPath;
+  }
+
+  /**
+   * Get the vendor file path.
+   *
+   * @return string
+   *   The vendor file path.
+   */
+  protected function getVendorPath() {
+    if (!$this->vendorPath) {
+      $this->vendorPath = $this->getRootPath() . DIRECTORY_SEPARATOR . 'vendor';
+    }
+    return $this->vendorPath;
+  }
+
+  /**
+   * Determine if the root and vendor file system are the same logical disk.
+   *
+   * @param string $root
+   *   Root file path.
+   * @param string $vendor
+   *   Vendor file path.
+   *
+   * @return bool
+   *   TRUE if same file system, FALSE otherwise.
+   */
+  protected function areSameLogicalDisk(string $root, string $vendor) {
+    $root_statistics = stat($root);
+    $vendor_statistics = stat($vendor);
+    return $root_statistics && $vendor_statistics && $root_statistics['dev'] === $vendor_statistics['dev'];
+  }
+
+}
diff --git a/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerInterface.php b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerInterface.php
new file mode 100644
index 0000000000..11fe60ae39
--- /dev/null
+++ b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerInterface.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\automatic_updates\ReadinessChecker;
+
+/**
+ * Interface for objects capable of readiness checking.
+ */
+interface ReadinessCheckerInterface {
+
+  /**
+   * Run check.
+   *
+   * @return array
+   *   An array of translatable strings.
+   */
+  public function run();
+
+}
diff --git a/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManager.php b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManager.php
new file mode 100644
index 0000000000..c69f841de6
--- /dev/null
+++ b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManager.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Drupal\automatic_updates\ReadinessChecker;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
+
+/**
+ * Defines a chained readiness checker implementation combining multiple checks.
+ */
+class ReadinessCheckerManager implements ReadinessCheckerManagerInterface {
+
+  /**
+   * 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\automatic_updates\ReadinessChecker\ReadinessCheckerInterface[][][]
+   */
+  protected $checkers = [];
+
+  /**
+   * ReadinessCheckerManager constructor.
+   *
+   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value
+   *   The key/value service.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(KeyValueFactoryInterface $key_value, ConfigFactoryInterface $config_factory) {
+    $this->keyValue = $key_value->get('automatic_updates');
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addChecker(ReadinessCheckerInterface $checker, string $category = 'warning', $priority = 0) {
+    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(string $category) {
+    if (!$this->isEnabled()) {
+      return [];
+    }
+    $messages = [];
+
+    if (!isset($this->getSortedCheckers()[$category])) {
+      // @todd Adding only 1 checker so will error for "warning".
+      // throw new \InvalidArgumentException(sprintf('No readiness checkers exist of category "%s"', $category));
+    }
+
+    foreach ($this->getSortedCheckers()[$category] as $checker) {
+      $messages[] = $checker->run();
+    }
+    $messages = array_merge(...$messages);
+    $this->keyValue->set("readiness_check_results.$category", $messages);
+    $this->keyValue->set('readiness_check_timestamp', \Drupal::time()->getRequestTime());
+    return $messages;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getResults($category) {
+    $results = [];
+    if ($this->isEnabled()) {
+      $results = $this->keyValue->get("readiness_check_results.$category", []);
+    }
+    return $results;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function timestamp() {
+    $last_check_timestamp = $this->keyValue->get('readiness_check_timestamp');
+    if (!is_numeric($last_check_timestamp)) {
+      $last_check_timestamp = \Drupal::state()->get('install_time', 0);
+    }
+    return $last_check_timestamp;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEnabled() {
+    return $this->configFactory->get('automatic_updates.settings')->get('enable_readiness_checks');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategories() {
+    return [self::ERROR, self::WARNING];
+  }
+
+  /**
+   * Sorts checkers according to priority.
+   *
+   * @return \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerInterface[]
+   *   A sorted array of checker objects.
+   */
+  protected function getSortedCheckers() {
+    $sorted = [];
+    foreach ($this->checkers as $category => $priorities) {
+      foreach ($priorities as $checkers) {
+        krsort($checkers);
+        $sorted[$category][] = $checkers;
+      }
+      $sorted[$category] = array_merge(...$sorted[$category]);
+    }
+    return $sorted;
+  }
+
+}
diff --git a/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
new file mode 100644
index 0000000000..b656943f30
--- /dev/null
+++ b/core/modules/automatic_updates/src/ReadinessChecker/ReadinessCheckerManagerInterface.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\automatic_updates\ReadinessChecker;
+
+/**
+ * Readiness checker manager interface.
+ */
+interface ReadinessCheckerManagerInterface {
+
+  /**
+   * Error category.
+   */
+  const ERROR = 'error';
+
+  /**
+   * Warning category.
+   */
+  const WARNING = 'warning';
+
+  /**
+   * Last checked ago warning (in seconds).
+   */
+  const LAST_CHECKED_WARNING = 3600 * 24;
+
+  /**
+   * Appends a checker to the checker chain.
+   *
+   * @param \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerInterface $checker
+   *   The checker interface to be appended to the checker chain.
+   * @param string $category
+   *   (optional) The category of check.
+   * @param int $priority
+   *   (optional) The priority of the checker being added.
+   *
+   * @return $this
+   */
+  public function addChecker(ReadinessCheckerInterface $checker, string $category = 'warning', $priority = 0);
+
+  /**
+   * Run checks.
+   *
+   * @param string $category
+   *   The category of check.
+   *
+   * @return array
+   *   An array of translatable strings.
+   */
+  public function run(string $category);
+
+  /**
+   * Get results of most recent run.
+   *
+   * @param string $category
+   *   The category of check.
+   *
+   * @return array
+   *   An array of translatable strings.
+   */
+  public function getResults($category);
+
+  /**
+   * Get timestamp of most recent run.
+   *
+   * @return int
+   *   A unix timestamp of most recent completed run.
+   */
+  public function timestamp();
+
+  /**
+   * Determine if readiness checks is enabled.
+   *
+   * @return bool
+   *   TRUE if enabled, otherwise FALSE.
+   */
+  public function isEnabled();
+
+  /**
+   * Get the checker categories.
+   *
+   * @return string[]
+   *   The checkers categories.
+   */
+  public function getCategories();
+
+}
diff --git a/core/modules/automatic_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php b/core/modules/automatic_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php
new file mode 100644
index 0000000000..7efc53affe
--- /dev/null
+++ b/core/modules/automatic_updates/tests/src/Kernel/ReadinessChecker/DiskSpaceTest.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\Tests\automatic_updates\Kernel\ReadinessChecker;
+
+use Drupal\automatic_updates\ReadinessChecker\DiskSpace;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests disk space readiness checking.
+ *
+ * @group automatic_updates
+ */
+class DiskSpaceTest extends KernelTestBase {
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['automatic_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);
+
+    // 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);
+  }
+
+}
+
+/**
+ * Class TestDiskSpace.
+ */
+class TestDiskSpace extends DiskSpace {
+
+  /**
+   * Override the default free disk space minimum to an insanely high number.
+   */
+  const MINIMUM_DISK_SPACE = 99999999999999999999999999999999999999999999999999;
+
+}
+
+/**
+ * Class TestDiskSpaceNonSameDisk.
+ */
+class TestDiskSpaceNonSameDisk extends TestDiskSpace {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function areSameLogicalDisk(string $root, string $vendor) {
+    return FALSE;
+  }
+
+}
