diff --git a/core/core.services.yml b/core/core.services.yml
index e5d8a263133ac60af129ac473a6df295049f376d..657920409ce7a2be1212842bf55f5e1b7b6bcacf 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -949,6 +949,14 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['@path.alias_manager', '@path_processor_manager', '@path.current']
+  route_access_response_subscriber:
+    class: Drupal\Core\EventSubscriber\RouteAccessResponseSubscriber
+    tags:
+      - { name: event_subscriber }
+  client_error_response_subscriber:
+    class: Drupal\Core\EventSubscriber\ClientErrorResponseSubscriber
+    tags:
+      - { name: event_subscriber }
   anonymous_user_response_subscriber:
     class: Drupal\Core\EventSubscriber\AnonymousUserResponseSubscriber
     tags:
diff --git a/core/lib/Drupal/Core/Cache/CacheableMetadata.php b/core/lib/Drupal/Core/Cache/CacheableMetadata.php
index d21bd650a082e7b19218ee012f63a1fe36182b36..0a22f56b420c4d03feeaaa96a76531a8511891a5 100644
--- a/core/lib/Drupal/Core/Cache/CacheableMetadata.php
+++ b/core/lib/Drupal/Core/Cache/CacheableMetadata.php
@@ -178,7 +178,10 @@ public static function createFromRenderArray(array $build) {
    * Creates a CacheableMetadata object from a depended object.
    *
    * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $object
-   *   The object whose cacheability metadata to retrieve.
+   *   The object whose cacheability metadata to retrieve. If it implements
+   *   CacheableDependencyInterface, its cacheability metadata will be used,
+   *   otherwise, the passed in object must be assumed to be uncacheable, so
+   *   max-age 0 is set.
    *
    * @return static
    */
diff --git a/core/lib/Drupal/Core/Cache/CacheableResponse.php b/core/lib/Drupal/Core/Cache/CacheableResponse.php
new file mode 100644
index 0000000000000000000000000000000000000000..2acac21c350bac3986c8862f574493d4cbb7b8d9
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableResponse.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheableResponse.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A response that contains and can expose cacheability metadata.
+ *
+ * Supports Drupal's caching concepts: cache tags for invalidation and cache
+ * contexts for variations.
+ *
+ * @see \Drupal\Core\Cache\Cache
+ * @see \Drupal\Core\Cache\CacheableMetadata
+ * @see \Drupal\Core\Cache\CacheableResponseTrait
+ */
+class CacheableResponse extends Response implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..0bca8d1fcff15af6c012b567949c24946b4f4a3a
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableResponseInterface.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheableResponseInterface.
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines an interface for responses that can expose cacheability metadata.
+ *
+ * @see \Drupal\Core\Cache\CacheableResponseTrait
+ */
+interface CacheableResponseInterface {
+
+  /**
+   * Adds a dependency on an object: merges its cacheability metadata.
+   *
+   * E.g. when a response depends on some configuration, an entity, or an access
+   * result, we must make sure their cacheability metadata is present on the
+   * response. This method makes doing that simple.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
+   *   The dependency. If the object implements CacheableDependencyInterface,
+   *   then its cacheability metadata will be used. Otherwise, the passed in
+   *   object must be assumed to be uncacheable, so max-age 0 is set.
+   *
+   * @return $this
+   *
+   * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
+   */
+  public function addCacheableDependency($dependency);
+
+  /**
+   * Returns the cacheability metadata for this response.
+   *
+   * @return \Drupal\Core\Cache\CacheableMetadata
+   */
+  public function getCacheableMetadata();
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..2940c1309736486cfd0205823098872172a28dff
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableResponseTrait.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Provides an implementation of CacheableResponseInterface.
+ *
+ * @see \Drupal\Core\Cache\CacheableResponseInterface
+ */
+trait CacheableResponseTrait {
+
+  /**
+   * The cacheability metadata.
+   *
+   * @var \Drupal\Core\Cache\CacheableMetadata
+   */
+  protected $cacheabilityMetadata;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addCacheableDependency($dependency) {
+    // A trait doesn't have a constructor, so initialize the cacheability
+    // metadata if that hasn't happened yet.
+    if (!isset($this->cacheabilityMetadata)) {
+      $this->cacheabilityMetadata = new CacheableMetadata();
+    }
+
+    $this->cacheabilityMetadata = $this->cacheabilityMetadata->merge(CacheableMetadata::createFromObject($dependency));
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheableMetadata() {
+    // A trait doesn't have a constructor, so initialize the cacheability
+    // metadata if that hasn't happened yet.
+    if (!isset($this->cacheabilityMetadata)) {
+      $this->cacheabilityMetadata = new CacheableMetadata();
+    }
+
+    return $this->cacheabilityMetadata;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
index fea9d9c36cbd262737991646aaaef2e31fd04a4a..ae4b46daed0311d6e6ea5f4c739fe08aaceef1bf 100644
--- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
+++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php
@@ -169,13 +169,13 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt
         // Associate the cache tags for the field definition & field storage
         // definition.
         $field_definition = $this->getFieldDefinition($name);
-        $this->renderer->addDependency($form[$name], $field_definition);
-        $this->renderer->addDependency($form[$name], $field_definition->getFieldStorageDefinition());
+        $this->renderer->addCacheableDependency($form[$name], $field_definition);
+        $this->renderer->addCacheableDependency($form[$name], $field_definition->getFieldStorageDefinition());
       }
     }
 
     // Associate the cache tags for the form display.
-    $this->renderer->addDependency($form, $this);
+    $this->renderer->addCacheableDependency($form, $this);
 
     // Add a process callback so we can assign weights and hide extra fields.
     $form['#process'][] = array($this, 'processForm');
diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
index 6712fb57146aa3dffd2d3e8fcf43c86807dcdd50..5b28f764f72bcb19df60e83350f78ac6010c851d 100644
--- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
+++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php
@@ -242,7 +242,7 @@ public function buildMultiple(array $entities) {
           $field_access = $items->access('view', NULL, TRUE);
           $build_list[$id][$name] = $field_access->isAllowed() ? $formatter->view($items) : [];
           // Apply the field access cacheability metadata to the render array.
-          $this->renderer->addDependency($build_list[$id][$name], $field_access);
+          $this->renderer->addCacheableDependency($build_list[$id][$name], $field_access);
         }
       }
     }
diff --git a/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php
index 3270d07b8f2a52768b9b050a2dff2f8489885d0b..0e646986c25fdbf2c5ed494666c6fd6ae7311b5b 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AnonymousUserResponseSubscriber.php
@@ -8,6 +8,8 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -52,6 +54,9 @@ public function onRespond(FilterResponseEvent $event) {
     }
 
     $response = $event->getResponse();
+    if (!$response instanceof CacheableResponseInterface) {
+      return;
+    }
 
     // The 'user.permissions' cache context ensures that if the permissions for
     // a role are modified, users are not served stale render cache content.
@@ -60,14 +65,10 @@ public function onRespond(FilterResponseEvent $event) {
     // be invalidated. Therefore, when varying by permissions and the current
     // user is the anonymous user, also add the cache tag for the 'anonymous'
     // role.
-    $cache_contexts = $response->headers->get('X-Drupal-Cache-Contexts');
-    if ($cache_contexts && in_array('user.permissions', explode(' ', $cache_contexts))) {
-      $cache_tags = ['config:user.role.anonymous'];
-      if ($response->headers->get('X-Drupal-Cache-Tags')) {
-        $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-        $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
-      }
-      $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
+    if (in_array('user.permissions', $response->getCacheableMetadata()->getCacheContexts())) {
+      $per_permissions_response_for_anon = new CacheableMetadata();
+      $per_permissions_response_for_anon->setCacheTags(['config:user.role.anonymous']);
+      $response->addCacheableDependency($per_permissions_response_for_anon);
     }
   }
 
@@ -78,7 +79,10 @@ public function onRespond(FilterResponseEvent $event) {
    *   An array of event listener definitions.
    */
   public static function getSubscribedEvents() {
-    $events[KernelEvents::RESPONSE][] = ['onRespond', -5];
+    // Priority 5, so that it runs before FinishResponseSubscriber, but after
+    // event subscribers that add the associated cacheability metadata (which
+    // have priority 10). This one is conditional, so must run after those.
+    $events[KernelEvents::RESPONSE][] = ['onRespond', 5];
     return $events;
   }
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..5b4f0bdc6d1422396500a3cb1596e895e100c217
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/ClientErrorResponseSubscriber.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\CacheableResponseSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Response subscriber to set the '4xx-response' cache tag on 4xx responses.
+ */
+class ClientErrorResponseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Sets the '4xx-response' cache tag on 4xx responses.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    if (!$event->isMasterRequest()) {
+      return;
+    }
+
+    $response = $event->getResponse();
+    if (!$response instanceof CacheableResponseInterface) {
+      return;
+    }
+
+    if ($response->isClientError()) {
+      $http_4xx_response_cacheability = new CacheableMetadata();
+      $http_4xx_response_cacheability->setCacheTags(['4xx-response']);
+      $response->addCacheableDependency($http_4xx_response_cacheability);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Priority 10, so that it runs before FinishResponseSubscriber, which will
+    // expose the cacheability metadata in the form of headers.
+    $events[KernelEvents::RESPONSE][] = ['onRespond', 10];
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 7fd88700b4b5784d8ca192afe8e8330067b35f0e..66283d8653f5ecfedfadf5a3761eceeb928a1c34 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -10,6 +10,8 @@
 use Drupal\Component\Datetime\DateTimePlus;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Cache\CacheContextsManager;
 use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -133,19 +135,12 @@ public function onRespond(FilterResponseEvent $event) {
       $response->headers->set($name, $value, FALSE);
     }
 
-    // Apply the request's access result cacheability metadata, if it has any.
-    $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
-    if ($access_result instanceof CacheableDependencyInterface) {
-      $this->updateDrupalCacheHeaders($response, $access_result);
-    }
-    // Add a cache tag to any 4xx response.
-    if ($response->isClientError()) {
-      $cache_tags = ['4xx-response'];
-      if ($response->headers->has('X-Drupal-Cache-Tags')) {
-        $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-        $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
-      }
-      $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
+    // Expose the cache contexts and cache tags associated with this page in a
+    // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively.
+    if ($response instanceof CacheableResponseInterface) {
+      $response_cacheability = $response->getCacheableMetadata();
+      $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags()));
+      $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts())));
     }
 
     $is_cacheable = ($this->requestPolicy->check($request) === RequestPolicyInterface::ALLOW) && ($this->responsePolicy->check($response, $request) !== ResponsePolicyInterface::DENY);
@@ -167,30 +162,6 @@ public function onRespond(FilterResponseEvent $event) {
     }
   }
 
-  /**
-   * Updates Drupal's cache headers using the route's cacheable access result.
-   *
-   * @param \Symfony\Component\HttpFoundation\Response $response
-   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheable_access_result
-   */
-  protected function updateDrupalCacheHeaders(Response $response, CacheableDependencyInterface $cacheable_access_result) {
-    // X-Drupal-Cache-Tags
-    $cache_tags = $cacheable_access_result->getCacheTags();
-    if ($response->headers->has('X-Drupal-Cache-Tags')) {
-      $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-      $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
-    }
-    $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
-
-    // X-Drupal-Cache-Contexts
-    $cache_contexts = $cacheable_access_result->getCacheContexts();
-    if ($response->headers->has('X-Drupal-Cache-Contexts')) {
-      $existing_cache_contexts = explode(' ', $response->headers->get('X-Drupal-Cache-Contexts'));
-      $cache_contexts = Cache::mergeContexts($existing_cache_contexts, $cache_contexts);
-    }
-    $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($cache_contexts)));
-  }
-
   /**
    * Determine whether the given response has a custom Cache-Control header.
    *
diff --git a/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php
new file mode 100644
index 0000000000000000000000000000000000000000..76784e742fef17ef6fdbf062eca3686ef0b246c2
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/RouteAccessResponseSubscriber.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\CacheableResponseSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Response subscriber to bubble the route's access result's cacheability.
+ *
+ * During routing, access checking is performed. The corresponding access result
+ * is stored in the Request object's attributes, just like the matching route
+ * object is. In case of a cacheable response, the route's access result also
+ * determined the content of the response, and therefore the cacheability of the
+ * route's access result should also be applied to the resulting response.
+ *
+ * @see \Drupal\Core\Routing\AccessAwareRouterInterface::ACCESS_RESULT
+ * @see \Drupal\Core\Routing\AccessAwareRouter::matchRequest()
+ * @see \Drupal\Core\Routing\AccessAwareRouter::checkAccess()
+ */
+class RouteAccessResponseSubscriber implements EventSubscriberInterface {
+
+  /**
+   * Bubbles the route's access result' cacheability metadata.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    if (!$event->isMasterRequest()) {
+      return;
+    }
+
+    $response = $event->getResponse();
+    if (!$response instanceof CacheableResponseInterface) {
+      return;
+    }
+
+    $request = $event->getRequest();
+    $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
+    $response->addCacheableDependency($access_result);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Priority 10, so that it runs before FinishResponseSubscriber, which will
+    // expose the cacheability metadata in the form of headers.
+    $events[KernelEvents::RESPONSE][] = ['onRespond', 10];
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 9f73f1176abc9e43c799e78e5c7ec7bce2f0f482..4d8da4de8db7ec34521f31d5752bb5293df1ffe9 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -10,6 +10,8 @@
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Cache\CacheContextsManager;
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Display\PageVariantInterface;
@@ -161,27 +163,27 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
     }
     $content = $this->renderer->render($html);
 
-    // Expose the cache contexts and cache tags associated with this page in a
-    // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively. Also
-    // associate the "rendered" cache tag. This allows us to invalidate the
-    // entire render cache, regardless of the cache bin.
-    $cache_contexts = [];
-    $cache_tags = ['rendered'];
+    // Set the generator in the HTTP header.
+    list($version) = explode('.', \Drupal::VERSION, 2);
+
+    $response = new CacheableResponse($content, 200,[
+      'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
+    ]);
+
+    // Bubble the cacheability metadata associated with the rendered render
+    // arrays to the response.
     foreach (['page_top', 'page', 'page_bottom'] as $region) {
       if (isset($html[$region])) {
-        $cache_contexts = Cache::mergeContexts($cache_contexts, $html[$region]['#cache']['contexts']);
-        $cache_tags = Cache::mergeTags($cache_tags, $html[$region]['#cache']['tags']);
+        $response->addCacheableDependency(CacheableMetadata::createFromRenderArray($html[$region]));
       }
     }
 
-    // Set the generator in the HTTP header.
-    list($version) = explode('.', \Drupal::VERSION, 2);
+    // Also associate the "rendered" cache tag. This allows us to invalidate the
+    // entire render cache, regardless of the cache bin.
+    $default = new CacheableMetadata();
+    $default->setCacheTags(['rendered']);
+    $response->addCacheableDependency($default);
 
-    $response = new Response($content, 200,[
-      'X-Drupal-Cache-Tags' => implode(' ', $cache_tags),
-      'X-Drupal-Cache-Contexts' => implode(' ', $this->cacheContextsManager->optimizeTokens($cache_contexts)),
-      'X-Generator' => 'Drupal ' . $version . ' (https://www.drupal.org)'
-    ]);
     return $response;
   }
 
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index ac1bd6e7905311c05d623c84898e82d11816f9c7..b969a518b48d0eaeecf512148dbf9d7544c11074 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -497,7 +497,7 @@ public function mergeBubbleableMetadata(array $a, array $b) {
   /**
    * {@inheritdoc}
    */
-  public function addDependency(array &$elements, $dependency) {
+  public function addCacheableDependency(array &$elements, $dependency) {
     $meta_a = CacheableMetadata::createFromRenderArray($elements);
     $meta_b = CacheableMetadata::createFromObject($dependency);
     $meta_a->merge($meta_b)->applyTo($elements);
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 7e97526d88262bff00c8fca9af8b0c23a9a3ba19..0af066e28a32b1f41c6b5ae804f43fd18b58fe31 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -334,9 +334,13 @@ public function mergeBubbleableMetadata(array $a, array $b);
    * @param array &$elements
    *   The render array to update.
    * @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
-   *   The dependency.
+   *   The dependency. If the object implements CacheableDependencyInterface,
+   *   then its cacheability metadata will be used. Otherwise, the passed in
+   *   object must be assumed to be uncacheable, so max-age 0 is set.
+   *
+   * @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
    */
-  public function addDependency(array &$elements, $dependency);
+  public function addCacheableDependency(array &$elements, $dependency);
 
   /**
    * Merges two attachments arrays (which live under the '#attached' key).
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index 41daf56383155cccaea5c0bc9afaf56f101e400c..aab9611f8017b56ef9645c9c76065b7fafe9c513 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -362,7 +362,7 @@ protected function addParentSelectFormElements(array $book_link) {
         '#suffix' => '</div>',
       );
     }
-    $this->renderer->addDependency($form, $config);
+    $this->renderer->addCacheableDependency($form, $config);
 
     return $form;
   }
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index 10c2152d34a935bbfe5bed5c0fbb9541a996dfb1..9f4751923da07e36e2164a0a449a0c463a817301 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -225,9 +225,9 @@ public function form(array $form, FormStateInterface $form_state) {
       '#access' => $is_admin,
     );
 
-    $this->renderer->addDependency($form, $config);
+    $this->renderer->addCacheableDependency($form, $config);
     // The form depends on the field definition.
-    $this->renderer->addDependency($form, $field_definition->getConfig($entity->bundle()));
+    $this->renderer->addCacheableDependency($form, $field_definition->getConfig($entity->bundle()));
 
     return parent::form($form, $form_state, $comment);
   }
diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php
index 44d23df46fa0824c8453631ae08344b7f69ba24a..7233bb8747931871085369398defc763b79f8e4b 100644
--- a/core/modules/contact/src/Controller/ContactController.php
+++ b/core/modules/contact/src/Controller/ContactController.php
@@ -89,7 +89,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
     $form = $this->entityFormBuilder()->getForm($message);
     $form['#title'] = SafeMarkup::checkPlain($contact_form->label());
     $form['#cache']['contexts'][] = 'user.permissions';
-    $this->renderer->addDependency($form, $config);
+    $this->renderer->addCacheableDependency($form, $config);
     return $form;
   }
 
diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php
index 17c0454791e53f976cdc4c392e876d955116ca21..20a13de4aa6ca54887402bd05f56bb9efd4eab54 100644
--- a/core/modules/forum/src/Controller/ForumController.php
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -203,7 +203,7 @@ protected function build($forums, TermInterface $term, $topics = array(), $paren
     if (empty($term->forum_container->value)) {
       $build['#attached']['feed'][] = array('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->getName());
     }
-    $this->renderer->addDependency($build, $config);
+    $this->renderer->addCacheableDependency($build, $config);
 
     return [
       'action' => $this->buildActionLinks($config->get('vocabulary'), $term),
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 2c5eea96d4c0b15aa5cb25df9db7b91eecfa707e..448a353b66124b1736163001d0a4004b17451ebe 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -55,7 +55,11 @@ public function get(EntityInterface $entity) {
       }
     }
 
-    return new ResourceResponse($entity, 200, ['X-Drupal-Cache-Tags' => implode(' ', $entity->getCacheTags())]);
+    $response = new ResourceResponse($entity, 200);
+    // Make the response use the entity's cacheability metadata.
+    // @todo include access cacheability metadata, for the access checks above.
+    $response->addCacheableDependency($entity);
+    return $response;
   }
 
   /**
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index b962b3266f10b8341a49b0270143deb37ca73d55..f8f408f26b9e8af933be00e9010c6d37c4ddf463 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -109,14 +109,8 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
       $output = $serializer->serialize($data, $format);
       $response->setContent($output);
       $response->headers->set('Content-Type', $request->getMimeType($format));
-      // Add cache tags, but do not overwrite any that exist already on the
-      // response object.
-      $cache_tags = $this->container->get('config.factory')->get('rest.settings')->getCacheTags();
-      if ($response->headers->has('X-Drupal-Cache-Tags')) {
-        $existing_cache_tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-        $cache_tags = Cache::mergeTags($existing_cache_tags, $cache_tags);
-      }
-      $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $cache_tags));
+      // Add rest settings config's cache tags.
+      $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings'));
     }
     return $response;
   }
diff --git a/core/modules/rest/src/ResourceResponse.php b/core/modules/rest/src/ResourceResponse.php
index 23e9d47cc7ba65a2ac87ca8951d8d917455eeab6..2919fb16cde27458dd69ce129b273bfbd210fbde 100644
--- a/core/modules/rest/src/ResourceResponse.php
+++ b/core/modules/rest/src/ResourceResponse.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheableResponseTrait;
 use Symfony\Component\HttpFoundation\Response;
 
 /**
@@ -17,7 +19,9 @@
  * string or an object with a __toString() method, which is not a requirement
  * for data used here.
  */
-class ResourceResponse extends Response {
+class ResourceResponse extends Response implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
 
   /**
    * Response data that should be serialized.
diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php
index e0a4f3407ed65d92c341199e156ed5160c8220ee..e7bc5f058938d49783fe16bbb217297639a30e1f 100644
--- a/core/modules/system/src/Tests/Routing/RouterTest.php
+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -71,11 +71,21 @@ public function testFinishResponseSubscriber() {
     // 3. controller result: Response object, globally cacheable route access.
     $this->drupalGet('router_test/test1');
     $headers = $this->drupalGetHeaders();
-    $this->assertEqual($headers['x-drupal-cache-contexts'], '');
-    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
+    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
     // 4. controller result: Response object, per-role cacheable route access.
     $this->drupalGet('router_test/test20');
     $headers = $this->drupalGetHeaders();
+    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
+    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
+    // 5. controller result: CacheableResponse object, globally cacheable route access.
+    $this->drupalGet('router_test/test21');
+    $headers = $this->drupalGetHeaders();
+    $this->assertEqual($headers['x-drupal-cache-contexts'], '');
+    $this->assertEqual($headers['x-drupal-cache-tags'], '');
+    // 6. controller result: CacheableResponse object, per-role cacheable route access.
+    $this->drupalGet('router_test/test22');
+    $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
     $this->assertEqual($headers['x-drupal-cache-tags'], '');
   }
diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml
index 08dd057379ae04bcd5e4b4348570afffede545af..5debfc219c08ed961254a96ead4b39d0b89a86b5 100644
--- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml
+++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml
@@ -127,6 +127,20 @@ router_test.20:
   requirements:
     _role: 'anonymous'
 
+router_test.21:
+  path: '/router_test/test21'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test21'
+  requirements:
+    _access: 'TRUE'
+
+router_test.22:
+  path: '/router_test/test22'
+  defaults:
+    _controller: '\Drupal\router_test\TestControllers::test21'
+  requirements:
+    _role: 'anonymous'
+
 router_test.hierarchy_parent:
   path: '/menu-test/parent'
   defaults:
diff --git a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
index f2a1fd201dcc84e0884f43534df7bda6c78fc8c3..ae3b050290e12fdc3458a322bcb1b907d219c05c 100644
--- a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
+++ b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\router_test;
 
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\ParamConverter\ParamNotConvertedException;
 use Drupal\user\UserInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
@@ -97,6 +98,10 @@ public function test18() {
     ];
   }
 
+  public function test21() {
+    return new CacheableResponse('test21');
+  }
+
   /**
    * Throws an exception.
    *
diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
index 87d813d9b22eca943aa9ddcac699b9d8b4152b8f..5ac7ca01c6de99d9913a860b1d7415c0333c587f 100644
--- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
+++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php
@@ -287,7 +287,7 @@ public function permissionDependentContent() {
 
     // The content depends on the access result.
     $access = AccessResult::allowedIfHasPermission($this->currentUser, 'pet llamas');
-    $this->renderer->addDependency($build, $access);
+    $this->renderer->addCacheableDependency($build, $access);
 
     // Build the content.
     if ($access->isAllowed()) {
diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php
index 76736122f300f6946bb148a60247e69a20f7b5ef..94ec9cff04cbe0f2854bc242689dfc38ef8d4988 100644
--- a/core/modules/user/src/Form/UserLoginForm.php
+++ b/core/modules/user/src/Form/UserLoginForm.php
@@ -124,7 +124,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#validate'][] = '::validateAuthentication';
     $form['#validate'][] = '::validateFinal';
 
-    $this->renderer->addDependency($form, $config);
+    $this->renderer->addCacheableDependency($form, $config);
 
     return $form;
   }
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 617a44986457c851755237a47859f79c3eca441e..e8368b230dd56afbb062177a903803ee5992034b 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -622,16 +622,16 @@ public function providerTestRenderCacheMaxAge() {
   }
 
   /**
-   * @covers ::addDependency
+   * @covers ::addCacheableDependency
    *
-   * @dataProvider providerTestAddDependency
+   * @dataProvider providerTestAddCacheableDependency
    */
-  public function testAddDependency(array $build, $object, array $expected) {
-    $this->renderer->addDependency($build, $object);
+  public function testAddCacheableDependency(array $build, $object, array $expected) {
+    $this->renderer->addCacheableDependency($build, $object);
     $this->assertEquals($build, $expected);
   }
 
-  public function providerTestAddDependency() {
+  public function providerTestAddCacheableDependency() {
     return [
       // Empty render array, typical default cacheability.
       [