diff --git a/core/core.services.yml b/core/core.services.yml
index e39252d..dd5c4fd 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -16,6 +16,7 @@ parameters:
       tags: []
   factory.keyvalue:
     default: keyvalue.database
+  http.response.debug_cacheability_headers: false
   factory.keyvalue.expirable:
     default: keyvalue.expirable.database
   filter_protocols:
@@ -1070,7 +1071,7 @@ 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', '@cache_contexts_manager', '%http.response.debug_cacheability_headers%']
   response_generator_subscriber:
     class: Drupal\Core\EventSubscriber\ResponseGeneratorSubscriber
     tags:
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index b625f59..a373688 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -62,6 +62,13 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   protected $cacheContexts;
 
   /**
+   * Whether to send cacheability headers for debugging purposes.
+   *
+   * @var bool
+   */
+  protected $debugCacheabilityHeaders = FALSE;
+
+  /**
    * Constructs a FinishResponseSubscriber object.
    *
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
@@ -74,13 +81,16 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
    *   A policy rule determining the cacheability of a response.
    * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
    *   The cache contexts manager service.
+   * @param bool $http_response_debug_cacheability_headers
+   *   (optional) Whether to send cacheability headers for debugging purposes.
    */
-  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, CacheContextsManager $cache_contexts_manager, $http_response_debug_cacheability_headers = FALSE) {
     $this->languageManager = $language_manager;
     $this->config = $config_factory->get('system.performance');
     $this->requestPolicy = $request_policy;
     $this->responsePolicy = $response_policy;
     $this->cacheContextsManager = $cache_contexts_manager;
+    $this->debugCacheabilityHeaders = $http_response_debug_cacheability_headers;
   }
 
   /**
@@ -130,11 +140,13 @@ public function onRespond(FilterResponseEvent $event) {
       return;
     }
 
-    // Expose the cache contexts and cache tags associated with this page in a
-    // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively.
-    $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())));
+    if ($this->debugCacheabilityHeaders) {
+      // Expose the cache contexts and cache tags associated with this page in a
+      // X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags header respectively.
+      $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);
 
diff --git a/core/modules/page_cache/src/StackMiddleware/PageCache.php b/core/modules/page_cache/src/StackMiddleware/PageCache.php
index 23ddb84..76852e3 100644
--- a/core/modules/page_cache/src/StackMiddleware/PageCache.php
+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -8,6 +8,7 @@
 namespace Drupal\page_cache\StackMiddleware;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\PageCache\RequestPolicyInterface;
 use Drupal\Core\PageCache\ResponsePolicyInterface;
@@ -208,10 +209,19 @@ protected function lookup(Request $request, $type = self::MASTER_REQUEST, $catch
   protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch = TRUE) {
     $response = $this->httpKernel->handle($request, $type, $catch);
 
+    // Page Cache only works with cacheable responses. It does not work with
+    // plain Response objects.
+    if (!$response instanceof CacheableResponseInterface) {
+      return $response;
+    }
+
     // Currently it is not possible to cache some types of responses. Therefore
     // exclude binary file responses (generated files, e.g. images with image
     // styles) and streamed responses (files directly read from the disk).
     // see: https://github.com/symfony/symfony/issues/9128#issuecomment-25088678
+    // Note that these responses do not implement CacheableResponseInterface,
+    // but subclasses might, so this check is still necessary despite the code
+    // above.
     if ($response instanceof BinaryFileResponse || $response instanceof StreamedResponse) {
       return $response;
     }
@@ -220,12 +230,9 @@ protected function fetch(Request $request, $type = self::MASTER_REQUEST, $catch
       return $response;
     }
 
-    // Use the actual timestamp from an Expires header, if available.
-    $date = $response->getExpires()->getTimestamp();
-    $expire = ($date > time()) ? $date : Cache::PERMANENT;
-
-    $tags = explode(' ', $response->headers->get('X-Drupal-Cache-Tags'));
-    $this->set($request, $response, $expire, $tags);
+    $max_age = $response->getCacheableMetadata()->getCacheMaxAge();
+    $expire = ($max_age === Cache::PERMANENT) ? Cache::PERMANENT : (int) $request->server->get('REQUEST_TIME') + $max_age;
+    $this->set($request, $response, $expire, $response->getCacheableMetadata()->getCacheTags());
 
     // Mark response as a cache miss.
     $response->headers->set('X-Drupal-Cache', 'MISS');
diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php
index a2278e7..72c89ff 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -80,6 +80,37 @@ function testPageCacheTags() {
   }
 
   /**
+   * Test that the page cache doesn't depend on cacheability headers.
+   */
+  function testPageCacheTagsIndependentFromCacheabilityHeaders() {
+    $this->setHttpResponseDebugCacheabilityHeaders(FALSE);
+
+    $path = 'system-test/cache_tags_page';
+    $tags = array('system_test_cache_tags_page');
+    $this->drupalGet($path);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+
+    // Verify a cache hit, but also the presence of the correct cache tags.
+    $this->drupalGet($path);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
+    $cid_parts = array(\Drupal::url('system_test.cache_tags_page', array(), array('absolute' => TRUE)), 'html');
+    $cid = implode(':', $cid_parts);
+    $cache_entry = \Drupal::cache('render')->get($cid);
+    sort($cache_entry->tags);
+    $expected_tags = array(
+      'config:user.role.anonymous',
+      'pre_render',
+      'rendered',
+      'system_test_cache_tags_page',
+    );
+    $this->assertIdentical($cache_entry->tags, $expected_tags);
+
+    Cache::invalidateTags($tags);
+    $this->drupalGet($path);
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
+  }
+
+  /**
    * Tests support for different cache items with different request formats
    * specified via a query parameter.
    */
@@ -420,15 +451,25 @@ public function testCacheableResponseResponses() {
 
     // Try to fill the cache.
     $this->drupalGet('/system-test/respond-reponse');
-    $this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.');
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent');
 
     // Still not cached, uncacheable response.
     $this->drupalGet('/system-test/respond-reponse');
-    $this->assertFalse(in_array('X-Drupal-Cache', $this->drupalGetHeaders()), 'Drupal page cache header not found');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.');
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent');
 
     // Try to fill the cache.
+    $this->drupalGet('/system-test/respond-public-response');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.');
+    $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=60, public', 'Cache-Control header was sent');
+
+    // Still not cached, uncacheable response.
+    $this->drupalGet('/system-test/respond-public-response');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.');
+    $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=60, public', 'Cache-Control header was sent');
+
+    // Try to fill the cache.
     $this->drupalGet('/system-test/respond-cacheable-reponse');
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.');
@@ -444,12 +485,8 @@ public function testCacheableResponseResponses() {
       ->uninstall(['page_cache']);
 
     // Try to fill the cache.
-    $this->drupalGet('/system-test/respond-reponse');
-    $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent');
-
-    // Still not cached, uncacheable response.
-    $this->drupalGet('/system-test/respond-reponse');
-    $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent');
+    $this->drupalGet('/respond-cacheable-reponse');
+    $this->assertFalse($this->drupalGetHeader('X-Drupal-Cache'), 'Drupal page cache header not found.');
   }
 
 }
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index b22f128..53cb86a 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -812,6 +812,10 @@ protected function initSettings() {
     // TestBase::restoreEnvironment() will delete the entire site directory.
     // Not using File API; a potential error must trigger a PHP warning.
     chmod(DRUPAL_ROOT . '/' . $this->siteDirectory, 0777);
+
+    // During tests, cacheable responses should get the debugging cacheability
+    // headers by default.
+    $this->setContainerParameter('http.response.debug_cacheability_headers', TRUE);
   }
 
   /**
@@ -3019,4 +3023,18 @@ protected function assertNoCacheTag($cache_tag) {
     $this->assertFalse(in_array($cache_tag, $cache_tags), "'" . $cache_tag . "' is absent in the X-Drupal-Cache-Tags header.");
   }
 
+  /**
+   * Enables/disables the cacheability headers.
+   *
+   * Sets the http.response.debug_cacheability_headers container parameter.
+   *
+   * @param bool $value
+   *   (optional) Whether the debugging cacheability headers should be sent.
+   */
+  protected function setHttpResponseDebugCacheabilityHeaders($value = TRUE) {
+    $this->setContainerParameter('http.response.debug_cacheability_headers', $value);
+    $this->rebuildContainer();
+    $this->resetAll();
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php
index 17345fa..5db3230 100644
--- a/core/modules/system/src/Tests/Routing/RouterTest.php
+++ b/core/modules/system/src/Tests/Routing/RouterTest.php
@@ -91,6 +91,18 @@ public function testFinishResponseSubscriber() {
     $headers = $this->drupalGetHeaders();
     $this->assertEqual($headers['x-drupal-cache-contexts'], 'user.roles');
     $this->assertEqual($headers['x-drupal-cache-tags'], '');
+
+    // Finally, verify that the X-Drupal-Cache-Contexts and X-Drupal-Cache-Tags
+    // headers are not sent when their container parameter is set to FALSE.
+    $this->drupalGet('router_test/test18');
+    $headers = $this->drupalGetHeaders();
+    $this->assertTrue(isset($headers['x-drupal-cache-contexts']));
+    $this->assertTrue(isset($headers['x-drupal-cache-tags']));
+    $this->setHttpResponseDebugCacheabilityHeaders(FALSE);
+    $this->drupalGet('router_test/test18');
+    $headers = $this->drupalGetHeaders();
+    $this->assertFalse(isset($headers['x-drupal-cache-contexts']));
+    $this->assertFalse(isset($headers['x-drupal-cache-tags']));
   }
 
   /**
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 abd9022..9d35102 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
@@ -276,6 +276,13 @@ public function respondWithReponse(Request $request) {
   }
 
   /**
+   * A plain Symfony reponse with Cache-Control: public, max-age=60.
+   */
+  public function respondWithPublicResponse() {
+    return (new Response('test'))->setPublic()->setMaxAge(60);
+  }
+
+  /**
    * A simple page callback that uses a CacheableResponse object.
    */
   public function respondWithCacheableReponse(Request $request) {
diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml
index 94a1bef..1fb4069 100644
--- a/core/modules/system/tests/modules/system_test/system_test.routing.yml
+++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml
@@ -137,6 +137,13 @@ system_test.respond_response:
   requirements:
     _access: 'TRUE'
 
+system_test.respond_public_response:
+  path: '/system-test/respond-public-response'
+  defaults:
+    _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithPublicResponse'
+  requirements:
+    _access: 'TRUE'
+
 system_test.respond_cacheable_response:
   path: '/system-test/respond-cacheable-reponse'
   defaults:
diff --git a/sites/default/default.services.yml b/sites/default/default.services.yml
index 4ab0662..23f6483 100644
--- a/sites/default/default.services.yml
+++ b/sites/default/default.services.yml
@@ -115,6 +115,17 @@ parameters:
       #
       # @default []
       tags: []
+  # Cacheability debugging:
+  #
+  # Responses with cacheability metadata (CacheableResponseInterface instances)
+  # get X-Drupal-Cache-Tags and X-Drupal-Cache-Contexts headers.
+  #
+  # For more information about debugging cacheable responses, see
+  # https://www.drupal.org/developing/api/8/response/cacheable-response-interface
+  #
+  # Not recommended in production environments
+  # @default false
+  http.response.debug_cacheability_headers: false
   factory.keyvalue:
     {}
     # Default key/value storage service to use.
