diff --git a/core/core.services.yml b/core/core.services.yml
index de88d89..38620b5 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1,4 +1,5 @@
 parameters:
+  header_value_size_limit: -1
   session.storage.options:
     gc_probability: 1
     gc_divisor: 100
@@ -1069,7 +1070,13 @@ services:
     class: Drupal\Core\EventSubscriber\FinishResponseSubscriber
     tags:
       - { name: event_subscriber }
-    arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy', '@cache_contexts_manager']
+    arguments: ['@language_manager', '@config.factory', '@page_cache_request_policy', '@page_cache_response_policy']
+  cacheable__response_subscriber:
+    class: Drupal\Core\EventSubscriber\CacheableResponseSubscriber
+    arguments: ['@cache_contexts_manager', '%header_value_size_limit%']
+    tags:
+      - { name: event_subscriber }
+      - { name: cache_tags_invalidator}
   response_generator_subscriber:
     class: Drupal\Core\EventSubscriber\ResponseGeneratorSubscriber
     tags:
diff --git a/core/lib/Drupal/Core/EventSubscriber/CacheableResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/CacheableResponseSubscriber.php
new file mode 100644
index 0000000..74ad87a
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/CacheableResponseSubscriber.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\CacheableResponseSubscriber.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+use Drupal\Core\Cache\Context\CacheContextsManager;
+use Drupal\Core\Site\Settings;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Response subscriber to send cacheability headers for cacheable responses.
+ */
+class CacheableResponseSubscriber implements EventSubscriberInterface, CacheTagsInvalidatorInterface {
+
+  /**
+   * The cache contexts manager service.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager
+   */
+  protected $cacheContextsManager;
+
+  /**
+   * The header value size limit (in bytes), -1 meaning no limit.
+   *
+   * @see sites/default/default.services.yml
+   *
+   * @var int
+   */
+  protected $limit;
+
+  /**
+   * Constructs a CacheableResponseSubscriber object.
+   *
+   * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
+   *   The cache contexts manager service.
+   * @param int $header_value_size_limit
+   *   The header value size limit.
+   */
+  public function __construct(CacheContextsManager $cache_contexts_manager, $header_value_size_limit) {
+    $this->cacheContextsManager = $cache_contexts_manager;
+    $this->limit = $header_value_size_limit;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateTags(array $tags) {
+    // When there is no limit, we never map cache tags to hashes.
+    if ($this->limit !== -1) {
+      // Also invalidate the cache tags as hashes, to automatically also work
+      // for cached responses that exceed the header size limit.
+      // @todo this causes infinite recursion… any ideas for a work-around?
+      Cache::invalidateTags(static::cacheTagsToHashes($tags));
+    }
+  }
+
+  /**
+   * Sends cacheability headers for responses providing cacheability metadata.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    $response = $event->getResponse();
+
+    // Cacheability headers can only be sent for responses providing
+    // cacheability metadata.
+    if (!$response instanceof CacheableResponseInterface) {
+      return;
+    }
+
+    // Only send cacheability headers when:
+    // - behind a reverse proxy (so the reverse proxy or CDN can be invalidated)
+    // - developing/debugging
+    if (!Settings::get('reverse_proxy', FALSE) && !Settings::get('send_cacheability_headers', FALSE)) {
+      return;
+    }
+
+    $response_cacheability = $response->getCacheableMetadata();
+    $response->headers->set('X-Drupal-Cache-Contexts', implode(' ', $this->cacheContextsManager->optimizeTokens($response_cacheability->getCacheContexts())));
+    $response->headers->set('X-Drupal-Cache-Tags', implode(' ', $response_cacheability->getCacheTags()));
+
+    // If the header value's size is limited, and the X-Drupal-Cache-Tags header
+    // exceeds that limit, then hash cache tags to hashes so that it fits within
+    // the limit; at the cost of making debugging more difficult.
+    if ($this->limit !== -1) {
+      $cache_tags_header_value = $response->headers->get('X-Drupal-Cache-Tags');
+      if (strlen($cache_tags_header_value) > $this->limit) {
+        $response->headers->set('X-Drupal-Cache-Tags', implode(' ', static::cacheTagsToHashes($response_cacheability->getCacheTags())));
+      }
+    }
+  }
+
+  /**
+   * Maps cache tags to hashes.
+   *
+   * Used when the Surrogate-Key/X-Drupal-Cache-Tags header size otherwise
+   * exceeds 16 KB.
+   *
+   * @param string[] $cache_tags
+   *   The cache tags in the header.
+   *
+   * @return string[]
+   *   The hashes to use instead in the header.
+   */
+  public static function cacheTagsToHashes(array $cache_tags) {
+    $hashes = [];
+    foreach ($cache_tags as $cache_tag) {
+      $hashes[] = substr(md5($cache_tag), 0, 3);
+    }
+    return $hashes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::RESPONSE][] = ['onRespond'];
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 4007003..abab752 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -8,8 +8,6 @@
 namespace Drupal\Core\EventSubscriber;
 
 use Drupal\Component\Datetime\DateTimePlus;
-use Drupal\Core\Cache\CacheableResponseInterface;
-use Drupal\Core\Cache\Context\CacheContextsManager;
 use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
@@ -59,13 +57,6 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   protected $responsePolicy;
 
   /**
-   * The cache contexts manager service.
-   *
-   * @var \Drupal\Core\Cache\Context\CacheContextsManager
-   */
-  protected $cacheContexts;
-
-  /**
    * Constructs a FinishResponseSubscriber object.
    *
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -76,15 +67,12 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
    *   A policy rule determining the cacheability of a request.
    * @param \Drupal\Core\PageCache\ResponsePolicyInterface $response_policy
    *   A policy rule determining the cacheability of a response.
-   * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
-   *   The cache contexts manager service.
    */
-  public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy, CacheContextsManager $cache_contexts_manager) {
+  public function __construct(LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, RequestPolicyInterface $request_policy, ResponsePolicyInterface $response_policy) {
     $this->languageManager = $language_manager;
     $this->config = $config_factory->get('system.performance');
     $this->requestPolicy = $request_policy;
     $this->responsePolicy = $response_policy;
-    $this->cacheContextsManager = $cache_contexts_manager;
   }
 
   /**
@@ -115,14 +103,6 @@ public function onRespond(FilterResponseEvent $event) {
     $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE);
     $response->headers->set('X-Frame-Options', 'SAMEORIGIN', FALSE);
 
-    // 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);
 
     // Add headers necessary to specify whether the response should be cached by
diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml
index 4ab0662..b171526 100644
--- a/sites/default/default.services.yml
+++ b/sites/default/default.services.yml
@@ -1,4 +1,7 @@
 parameters:
+  # Header value size limit in bytes. -1 means infinite, no limit.
+  # @default -1
+  header_value_size_limit: -1
   session.storage.options:
     # Default ini options for sessions.
     #
