diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 0db7124a4a57efa0c882a444a03012d47b8da4b2..37a265f4ec87345a86b83e7a8be7cf5f86a2a020 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -27,6 +27,7 @@
 use Drupal\Core\Render\MainContent\MainContentRenderersPass;
 use Drupal\Core\Site\Settings;
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
+use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -142,6 +143,12 @@ protected function registerTest(ContainerBuilder $container) {
     $container
       ->register('test.http_client.middleware', 'Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware')
       ->addTag('http_client_middleware');
+    // Add the wait terminate middleware which acquires a lock to signal request
+    // termination to the test runner.
+    $container
+      ->register('test.http_middleware.wait_terminate_middleware', 'Drupal\Core\Test\StackMiddleware\TestWaitTerminateMiddleware')
+      ->setArguments([new Reference('state'), new Reference('lock')])
+      ->addTag('http_middleware', ['priority' => -1024]);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php b/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php
index 99bb449e2f07ce3fa0060ca0a5be4dce9a31ef6e..9b8d675b28f2d134fdaae940b2d3f8694a6dfef1 100644
--- a/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php
+++ b/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php
@@ -31,6 +31,13 @@ public function __invoke() {
             if (!drupal_valid_test_ua()) {
               return $response;
             }
+            if (!empty($response->getHeader('X-Drupal-Wait-Terminate')[0])) {
+              $lock = \Drupal::lock();
+              if (!$lock->acquire('test_wait_terminate')) {
+                $lock->wait('test_wait_terminate');
+              }
+              $lock->release('test_wait_terminate');
+            }
             $headers = $response->getHeaders();
             foreach ($headers as $header_name => $header_values) {
               if (preg_match('/^X-Drupal-Assertion-[0-9]+$/', $header_name, $matches)) {
diff --git a/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php b/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..58cc844024c4482c9e83edfd0ab986c399750256
--- /dev/null
+++ b/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Core\Test\StackMiddleware;
+
+use Drupal\Core\Lock\LockBackendInterface;
+use Drupal\Core\State\StateInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * Acquire a lock to signal request termination to the test runner.
+ */
+class TestWaitTerminateMiddleware implements HttpKernelInterface {
+
+  /**
+   * Constructs a test wait terminate stack middleware object.
+   *
+   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $httpKernel
+   *   The decorated kernel.
+   * @param \Drupal\Core\State\StateInterface $state
+   *   The state server.
+   * @param \Drupal\Core\Lock\LockBackendInterface $lock
+   *   The lock backend.
+   */
+  public function __construct(
+    protected HttpKernelInterface $httpKernel,
+    protected StateInterface $state,
+    protected LockBackendInterface $lock
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
+    $result = $this->httpKernel->handle($request, $type, $catch);
+
+    if ($this->state->get('drupal.test_wait_terminate')) {
+      // Set a header on the response to instruct the test runner that it must
+      // await the lock. Note that the lock acquired here is automatically
+      // released from within a shutdown function.
+      $this->lock->acquire('test_wait_terminate');
+      $result->headers->set('X-Drupal-Wait-Terminate', '1');
+    }
+
+    return $result;
+  }
+
+}
diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
index d0893201e1a875a4edcab376c89de20c2be6f05d..2496d242f23f4209cfa90701589596d92b6859f0 100644
--- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php
+++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php
@@ -11,6 +11,7 @@
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
+use Drupal\Tests\WaitTerminateTestTrait;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
 
@@ -22,6 +23,7 @@
 class NodeTest extends ResourceTestBase {
 
   use CommonCollectionFilterAccessTestPatternsTrait;
+  use WaitTerminateTestTrait;
 
   /**
    * {@inheritdoc}
@@ -315,6 +317,8 @@ public function testPatchPath() {
    * {@inheritdoc}
    */
   public function testGetIndividual() {
+    $this->setWaitForTerminate();
+
     parent::testGetIndividual();
 
     $this->assertCacheableNormalizations();
@@ -393,15 +397,11 @@ protected function assertCacheableNormalizations(): void {
     $request_options = $this->getAuthenticationRequestOptions();
     $request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'title']];
     $this->request('GET', $url, $request_options);
-    // Cacheable normalizations are written after the response is flushed to
-    // the client; give the server a chance to complete this work.
-    sleep(1);
     // Ensure the normalization cache is being incrementally built. After
     // requesting the title, only the title is in the cache.
     $this->assertNormalizedFieldsAreCached(['title']);
     $request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'field_rest_test']];
     $this->request('GET', $url, $request_options);
-    sleep(1);
     // After requesting an additional field, then that field is in the cache and
     // the old one is still there.
     $this->assertNormalizedFieldsAreCached(['title', 'field_rest_test']);
diff --git a/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php b/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php
index 2c1f529f5041043bd52ae556299e3c3cfe1f3cc7..eb11a0cd5f050138ccb498d6cc48ce23380858af 100644
--- a/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php
+++ b/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php
@@ -11,6 +11,7 @@
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\WaitTerminateTestTrait;
 
 /**
  * Tests Language Negotiation.
@@ -21,6 +22,8 @@
  */
 class ConfigurableLanguageManagerTest extends BrowserTestBase {
 
+  use WaitTerminateTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -45,6 +48,8 @@ class ConfigurableLanguageManagerTest extends BrowserTestBase {
   protected function setUp(): void {
     parent::setUp();
 
+    $this->setWaitForTerminate();
+
     /** @var \Drupal\user\UserInterface $user */
     $user = $this->createUser([], '', TRUE);
     $this->drupalLogin($user);
@@ -266,16 +271,4 @@ public function testUserProfileTranslationWithPreferredAdminLanguage() {
     $assert_session->pageTextNotContains($field_label_es);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function drupalGet($path, array $options = [], array $headers = []) {
-    $response = parent::drupalGet($path, $options, $headers);
-    // The \Drupal\locale\LocaleTranslation service clears caches after the
-    // response is flushed to the client; wait for Drupal to perform its
-    // termination work before continuing.
-    sleep(1);
-    return $response;
-  }
-
 }
diff --git a/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php b/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php
index 5b43ad50bf1ab7666825b85cc705682280434ebd..34e763e7ad136aa5ef1fc03a5860128a644a9707 100644
--- a/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php
+++ b/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Gettext\PoItem;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\WaitTerminateTestTrait;
 
 /**
  * Tests LocaleLookup.
@@ -13,6 +14,8 @@
  */
 class LocaleLocaleLookupTest extends BrowserTestBase {
 
+  use WaitTerminateTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -31,6 +34,8 @@ class LocaleLocaleLookupTest extends BrowserTestBase {
   protected function setUp(): void {
     parent::setUp();
 
+    $this->setWaitForTerminate();
+
     // Change the language default object to different values.
     ConfigurableLanguage::createFromLangcode('fr')->save();
     $this->config('system.site')->set('default_langcode', 'fr')->save();
@@ -68,11 +73,6 @@ public function testLanguageFallbackDefaults() {
    * @dataProvider providerTestFixOldPluralStyle
    */
   public function testFixOldPluralStyle($translation_value, $expected) {
-    // The \Drupal\locale\LocaleTranslation service stores localization cache
-    // data after the response is flushed to the client. We do not want to race
-    // with any string translations that may be saving from the login in
-    // ::setUp().
-    sleep(1);
     $string_storage = \Drupal::service('locale.storage');
     $string = $string_storage->findString(['source' => 'Member for', 'context' => '']);
     $lid = $string->getId();
diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php
index f6ebeb9be328cdc98a55db60470b88ab0377bbe7..080725afb5e6d79b3edb03c4ce857882a2f8e81d 100644
--- a/core/modules/path/tests/src/Functional/PathAliasTest.php
+++ b/core/modules/path/tests/src/Functional/PathAliasTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Database\Database;
 use Drupal\Core\Url;
+use Drupal\Tests\WaitTerminateTestTrait;
 
 /**
  * Tests modifying path aliases from the UI.
@@ -13,6 +14,8 @@
  */
 class PathAliasTest extends PathTestBase {
 
+  use WaitTerminateTestTrait;
+
   /**
    * Modules to enable.
    *
@@ -40,6 +43,8 @@ protected function setUp(): void {
       'access content overview',
     ]);
     $this->drupalLogin($web_user);
+
+    $this->setWaitForTerminate();
   }
 
   /**
@@ -66,9 +71,6 @@ public function testPathCache() {
     \Drupal::cache('data')->deleteAll();
     // Make sure the path is not converted to the alias.
     $this->drupalGet(trim($edit['path[0][value]'], '/'), ['alias' => TRUE]);
-    // The \Drupal\path_alias\AliasWhitelist service performs cache clears after
-    // Drupal has flushed the response to the client; wait for this to finish.
-    sleep(1);
     $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');
 
     // Visit the alias for the node and confirm a cache entry is created.
@@ -76,7 +78,6 @@ public function testPathCache() {
     // @todo Remove this once https://www.drupal.org/node/2480077 lands.
     Cache::invalidateTags(['rendered']);
     $this->drupalGet(trim($edit['alias[0][value]'], '/'));
-    sleep(1);
     $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.');
   }
 
diff --git a/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php b/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php
index d61b08f8f7ab360f751c42117c436ae157873d9c..6b5da5aec68c8bf8908c2f1547703d6e6e391b04 100644
--- a/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php
+++ b/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
+use Drupal\Tests\WaitTerminateTestTrait;
 use Drupal\workspaces\Entity\Workspace;
 
 /**
@@ -15,6 +16,7 @@
 class PathWorkspacesTest extends BrowserTestBase {
 
   use WorkspaceTestUtilities;
+  use WaitTerminateTestTrait;
 
   /**
    * {@inheritdoc}
@@ -67,6 +69,7 @@ protected function setUp(): void {
     \Drupal::entityTypeManager()->clearCachedDefinitions();
 
     $this->setupWorkspaceSwitcherBlock();
+    $this->setWaitForTerminate();
   }
 
   /**
@@ -106,11 +109,6 @@ public function testPathAliases() {
     // Publish the workspace and check that the alias can be accessed in Live.
     $stage->publish();
     $this->assertAccessiblePaths([$path]);
-
-    // The \Drupal\path_alias\AliasWhitelist service performs cache clears after
-    // Drupal has flushed the response to the client; wait for this to finish.
-    sleep(1);
-
     $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
   }
 
@@ -155,11 +153,6 @@ public function testPathAliasesUserSwitch() {
 
     $this->drupalLogout();
     $this->assertAccessiblePaths([$path]);
-
-    // The \Drupal\path_alias\AliasWhitelist service performs cache clears after
-    // Drupal has flushed the response to the client; wait for this to finish.
-    sleep(1);
-
     $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
   }
 
diff --git a/core/tests/Drupal/Tests/WaitTerminateTestTrait.php b/core/tests/Drupal/Tests/WaitTerminateTestTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..008b1555012801054f9055554a993a345d4e3ea6
--- /dev/null
+++ b/core/tests/Drupal/Tests/WaitTerminateTestTrait.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Drupal\Tests;
+
+/**
+ * Provides a method to enforce that requests will wait for the terminate event.
+ */
+trait WaitTerminateTestTrait {
+
+  /**
+   * Specify that subsequent requests must wait for the terminate event.
+   *
+   * The terminate event is fired after a response is sent to the user agent.
+   * Tests with assertions which operate on data computed during the terminate
+   * event need to enable this.
+   */
+  protected function setWaitForTerminate() {
+    $this->container->get('state')->set('drupal.test_wait_terminate', TRUE);
+  }
+
+}