diff --git a/core/includes/batch.inc b/core/includes/batch.inc
index 6d0c835..2eddb0f 100644
--- a/core/includes/batch.inc
+++ b/core/includes/batch.inc
@@ -279,11 +279,27 @@ function _batch_process() {
     // processed or all sets have been completed.
     $queue = _batch_queue($current_set);
 
-    // If we are in progressive mode, break processing after 1 second.
-    if ($batch['progressive'] && Timer::read('batch_processing') > 1000) {
-      // Record elapsed wall clock time.
-      $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
-      break;
+    if ($batch['progressive']) {
+      if (drupal_valid_test_ua()) {
+        // Limit maximum memory when we're under test to the same value as
+        // \Drupal\simpletest\WebTestBase::$memoryUsageTrigger to ensure that
+        // Drupal works under that limit.
+        // @see \Drupal\simpletest\WebTestBase::curlHeaderCallback
+        $maximum_memory = \Drupal::SIMPLETEST_MAXIMUM_MEMORY_USAGE;
+      }
+      else {
+        $maximum_memory = ini_get('memory_limit');
+      }
+      // Limit to 60% memory use.
+      $max_bytes = \Drupal\Component\Utility\Bytes::toInt($maximum_memory) * 0.4;
+
+      // If we are in progressive mode, break processing after 1 second or using
+      // 60% of available memory.
+      if (Timer::read('batch_processing') > 1000 || memory_get_usage(TRUE) > $max_bytes) {
+        // Record elapsed wall clock time.
+        $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
+        break;
+      }
     }
   }
 
diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php
index ad525dd..9bd2ee2 100644
--- a/core/lib/Drupal.php
+++ b/core/lib/Drupal.php
@@ -94,6 +94,14 @@ class Drupal {
   const CORE_MINIMUM_SCHEMA_VERSION = 8000;
 
   /**
+   * Simpletest maximum target memory usage.
+   *
+   * This is defined here because it is used by child sites which do not have
+   * Simpletest installed.
+   */
+  const SIMPLETEST_MAXIMUM_MEMORY_USAGE = '64M';
+
+  /**
    * The currently active container object, or NULL if not initialized yet.
    *
    * @var \Symfony\Component\DependencyInjection\ContainerInterface|null
diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php
index 17c87a3..6f17deb 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionTestSiteSubscriber.php
@@ -8,7 +8,9 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Core\Utility\Error;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
  * Custom handling of errors when in a system-under-test.
@@ -46,7 +48,7 @@ public function on500(GetResponseForExceptionEvent $event) {
 
     // When running inside the testing framework, we relay the errors
     // to the tested site by the way of HTTP headers.
-    if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
+    if ($this->canAddHeaders()) {
       // $number does not use drupal_static as it should not be reset
       // as it uniquely identifies each PHP error.
       static $number = 0;
@@ -64,4 +66,38 @@ public function on500(GetResponseForExceptionEvent $event) {
     }
   }
 
+  /**
+   * Handles response being sent.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    if ($this->canAddHeaders()) {
+      $event->getResponse()->headers->add([
+        'X-Drupal-Memory-Peak' => memory_get_peak_usage(TRUE),
+      ]);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = parent::getSubscribedEvents();
+    // Add a listener for outputting peak memory usage.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 100];
+    return $events;
+  }
+
+  /**
+   * Indicates whether headers should be added.
+   *
+   * @return bool
+   *   TRUE if headers should be added.
+   */
+  protected function canAddHeaders() {
+    return (defined('DRUPAL_TEST_IN_CHILD_SITE') && DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS));
+  }
+
 }
diff --git a/core/modules/block/js/block.js b/core/modules/block/js/block.js
index df9327b..b3c8c31 100644
--- a/core/modules/block/js/block.js
+++ b/core/modules/block/js/block.js
@@ -78,6 +78,78 @@
         return;
       }
 
+      /**
+       * Function to check empty regions and toggle classes based on this.
+       *
+       * @param {jQuery} table
+       *   The jQuery object representing the table to inspect.
+       * @param {jQuery} rowObject
+       *   The jQuery object representing the table row.
+       */
+      function checkEmptyRegions(table, rowObject) {
+        table.find('tr.region-message').each(function () {
+          var $this = $(this);
+          // If the dragged row is in this region, but above the message row,
+          // swap it down one space.
+          if ($this.prev('tr').get(0) === rowObject.element) {
+            // Prevent a recursion problem when using the keyboard to move rows
+            // up.
+            if ((rowObject.method !== 'keyboard' || rowObject.direction === 'down')) {
+              rowObject.swap('after', this);
+            }
+          }
+          // This region has become empty.
+          if ($this.next('tr').is(':not(.draggable)') || $this.next('tr').length === 0) {
+            $this.removeClass('region-populated').addClass('region-empty');
+          }
+          // This region has become populated.
+          else if ($this.is('.region-empty')) {
+            $this.removeClass('region-empty').addClass('region-populated');
+          }
+        });
+      }
+
+      /**
+       * Function to update the last placed row with the correct classes.
+       *
+       * @param {jQuery} table
+       *   The jQuery object representing the table to inspect.
+       * @param {jQuery} rowObject
+       *   The jQuery object representing the table row.
+       */
+      function updateLastPlaced(table, rowObject) {
+        // Remove the color-success class from new block if applicable.
+        table.find('.color-success').removeClass('color-success');
+
+        var $rowObject = $(rowObject);
+        if (!$rowObject.is('.drag-previous')) {
+          table.find('.drag-previous').removeClass('drag-previous');
+          $rowObject.addClass('drag-previous');
+        }
+      }
+
+      /**
+       * Update block weights in the given region.
+       *
+       * @param {jQuery} table
+       *   Table with draggable items.
+       * @param {string} region
+       *   Machine name of region containing blocks to update.
+       */
+      function updateBlockWeights(table, region) {
+        // Calculate minimum weight.
+        var weight = -Math.round(table.find('.draggable').length / 2);
+        // Update the block weights.
+        table.find('.region-' + region + '-message').nextUntil('.region-title')
+          .find('select.block-weight').val(function () {
+            // Increment the weight before assigning it to prevent using the
+            // absolute minimum available weight. This way we always have an
+            // unused upper and lower bound, which makes manually setting the
+            // weights easier for users who prefer to do it that way.
+            return ++weight;
+          });
+      }
+
       var table = $('#blocks');
       // Get the blocks tableDrag object.
       var tableDrag = Drupal.tableDrag.blocks;
@@ -143,70 +215,6 @@
           // Remove focus from selectbox.
           select.trigger('blur');
         });
-
-      var updateLastPlaced = function ($table, rowObject) {
-        // Remove the color-success class from new block if applicable.
-        $table.find('.color-success').removeClass('color-success');
-
-        var $rowObject = $(rowObject);
-        if (!$rowObject.is('.drag-previous')) {
-          $table.find('.drag-previous').removeClass('drag-previous');
-          $rowObject.addClass('drag-previous');
-        }
-      };
-
-      /**
-       * Update block weights in the given region.
-       *
-       * @param {jQuery} $table
-       *   Table with draggable items.
-       * @param {string} region
-       *   Machine name of region containing blocks to update.
-       */
-      var updateBlockWeights = function ($table, region) {
-        // Calculate minimum weight.
-        var weight = -Math.round($table.find('.draggable').length / 2);
-        // Update the block weights.
-        $table.find('.region-' + region + '-message').nextUntil('.region-title')
-          .find('select.block-weight').val(function () {
-            // Increment the weight before assigning it to prevent using the
-            // absolute minimum available weight. This way we always have an
-            // unused upper and lower bound, which makes manually setting the
-            // weights easier for users who prefer to do it that way.
-            return ++weight;
-          });
-      };
-
-      /**
-       * Checks empty regions and toggles classes based on this.
-       *
-       * @param {jQuery} table
-       *   The jQuery object representing the table to inspect.
-       * @param {jQuery} rowObject
-       *   The jQuery object representing the table row.
-       */
-      var checkEmptyRegions = function (table, rowObject) {
-        table.find('tr.region-message').each(function () {
-          var $this = $(this);
-          // If the dragged row is in this region, but above the message row,
-          // swap it down one space.
-          if ($this.prev('tr').get(0) === rowObject.element) {
-            // Prevent a recursion problem when using the keyboard to move rows
-            // up.
-            if ((rowObject.method !== 'keyboard' || rowObject.direction === 'down')) {
-              rowObject.swap('after', this);
-            }
-          }
-          // This region has become empty.
-          if ($this.next('tr').is(':not(.draggable)') || $this.next('tr').length === 0) {
-            $this.removeClass('region-populated').addClass('region-empty');
-          }
-          // This region has become populated.
-          else if ($this.is('.region-empty')) {
-            $this.removeClass('region-empty').addClass('region-populated');
-          }
-        });
-      };
     }
   };
 
diff --git a/core/modules/simpletest/src/Tests/BrokenSetUpTest.php b/core/modules/simpletest/src/Tests/BrokenSetUpTest.php
index e5c1ebb..74ac44e 100644
--- a/core/modules/simpletest/src/Tests/BrokenSetUpTest.php
+++ b/core/modules/simpletest/src/Tests/BrokenSetUpTest.php
@@ -30,6 +30,11 @@ class BrokenSetUpTest extends WebTestBase {
   public static $modules = array('simpletest');
 
   /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
+  /**
    * The path to the shared trigger file.
    *
    * @var string
diff --git a/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php b/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
index 544dabd..eac8f74 100644
--- a/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
+++ b/core/modules/simpletest/src/Tests/InstallationProfileModuleTestsTest.php
@@ -31,6 +31,11 @@ class InstallationProfileModuleTestsTest extends WebTestBase {
   protected $adminUser;
 
   /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
+  /**
    * Use the Testing profile.
    *
    * The Testing profile contains drupal_system_listing_compatible_test.test,
diff --git a/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php b/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php
index fd8d8e0..81c4b3c 100644
--- a/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php
+++ b/core/modules/simpletest/src/Tests/MissingCheckedRequirementsTest.php
@@ -23,6 +23,11 @@ class MissingCheckedRequirementsTest extends WebTestBase {
    */
   public static $modules = array('simpletest');
 
+  /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
   protected function setUp() {
     parent::setUp();
     $admin_user = $this->drupalCreateUser(array('administer unit tests'));
diff --git a/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php b/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
index 9cfbfff..9d03383 100644
--- a/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
+++ b/core/modules/simpletest/src/Tests/OtherInstallationProfileTestsTest.php
@@ -39,6 +39,11 @@ class OtherInstallationProfileTestsTest extends WebTestBase {
   protected $profile = 'minimal';
 
   /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
+  /**
    * An administrative user with permission to administer unit tests.
    *
    * @var \Drupal\user\UserInterface
diff --git a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
index 574da77..c76e315 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
@@ -24,6 +24,11 @@ class SimpleTestBrowserTest extends WebTestBase {
    */
   public static $modules = array('simpletest', 'test_page_test');
 
+  /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
   public function setUp() {
     parent::setUp();
     // Create and log in an admin user.
diff --git a/core/modules/simpletest/src/Tests/SimpleTestTest.php b/core/modules/simpletest/src/Tests/SimpleTestTest.php
index 41d114e..af931b3 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
@@ -8,7 +8,9 @@
 namespace Drupal\simpletest\Tests;
 
 use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
+use Drupal\user\Entity\User;
 
 /**
  * Tests SimpleTest's web interface: check that the intended tests were run and
@@ -27,6 +29,11 @@ class SimpleTestTest extends WebTestBase {
   public static $modules = ['simpletest'];
 
   /**
+   * Set no memory limit for this test. The SimpleTest UI is very expensive.
+   */
+  protected $memoryUsageTrigger = FALSE;
+
+  /**
    * The results array that has been parsed by getTestResults().
    *
    * @var array
@@ -230,6 +237,14 @@ function stubTest() {
 
     // This causes the debug message asserted in confirmStubResults().
     debug('Foo', 'Debug', FALSE);
+
+    // This causes \Drupal\simpletest\WebTestBase::curlHeaderCallback() to fail.
+    // Set an absurdly low limit and get a page.
+    $old_limit = $this->memoryUsageTrigger;
+    $this->memoryUsageTrigger = '1M';
+    // Getting the front page will result in a redirect to /user/2.
+    $this->drupalGet('<front>');
+    $this->memoryUsageTrigger = $old_limit;
   }
 
   /**
@@ -268,7 +283,12 @@ function confirmStubTestResults() {
 
     $this->assertAssertion("Debug: 'Foo'", 'Debug', 'Fail', 'SimpleTestTest.php', 'Drupal\simpletest\Tests\SimpleTestTest->stubTest()');
 
-    $this->assertEqual('16 passes, 3 fails, 2 exceptions, 3 debug messages', $this->childTestResults['summary']);
+    // Check that we have two exceeded memory errors. One for the frontpage and
+    // one for the user page which the request is redirected to.
+    $this->assertAssertion('exceeded acceptable maximum (1M) on url ' . Url::fromRoute('<front>')->setAbsolute()->toString(), 'Other', 'Fail', 'WebTestBase.php', 'Drupal\simpletest\WebTestBase->curlExec()');
+    $this->assertAssertion('exceeded acceptable maximum (1M) on url ' . User::load(2)->urlInfo()->setAbsolute()->toString(), 'Other', 'Fail', 'WebTestBase.php', 'Drupal\simpletest\WebTestBase->curlExec()');
+
+    $this->assertEqual('18 passes, 5 fails, 2 exceptions, 4 debug messages', $this->childTestResults['summary']);
 
     $this->testIds[] = $test_id = $this->getTestIdFromResults();
     $this->assertTrue($test_id, 'Found test ID in results.');
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 2edfc81..1d29493 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -11,6 +11,7 @@
 use Drupal\Component\FileCache\FileCacheFactory;
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Serialization\Yaml;
+use Drupal\Component\Utility\Bytes;
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\UrlHelper;
@@ -64,6 +65,20 @@
   protected $url;
 
   /**
+   * The maximum memory usage to allow on the child site, in MB.
+   *
+   * The minimum amount of memory Drupal expects to have available is defined by
+   * DRUPAL_MINIMUM_PHP_MEMORY_LIMIT. This default value on the base class
+   * should be less than or equal to DRUPAL_MINIMUM_PHP_MEMORY_LIMIT. If a
+   * module requires more memory than this limit, the module should add a
+   * check to its hook_requirements() and use the higher value in its tests.
+   * The check can be disabled for a specific test by setting it to FALSE.
+   *
+   * @var string|FALSE
+   */
+  protected $memoryUsageTrigger = \Drupal::SIMPLETEST_MAXIMUM_MEMORY_USAGE;
+
+  /**
    * The handle of the current cURL connection.
    *
    * @var resource
@@ -1455,6 +1470,13 @@ protected function curlHeaderCallback($curlHandler, $header) {
       // the header.
       call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
     }
+    if ($this->memoryUsageTrigger && preg_match('/^X-Drupal-Memory-Peak: (.*)$/', $header, $matches) && (float) $matches[1] >= Bytes::toInt($this->memoryUsageTrigger)) {
+      $this->fail(SafeMarkup::format('Child site memory usage (@site_usage) exceeded acceptable maximum (@max_usage) on url @url', [
+        '@url' => curl_getinfo($curlHandler, CURLINFO_EFFECTIVE_URL),
+        '@site_usage' => format_size($matches[1]),
+        '@max_usage' => $this->memoryUsageTrigger,
+      ]));
+    }
 
     // Save cookies.
     if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) {
