diff --git a/core/lib/Drupal/Core/Cache/CacheableJsonResponse.php b/core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
new file mode 100644
index 0000000..bc7d2a3
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableJsonResponse.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\CacheableResponse.
+ */
+
+namespace Drupal\Core\Cache;
+
+use Symfony\Component\HttpFoundation\JsonResponse;
+
+/**
+ * A JsonResponse 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 CacheableJsonResponse extends JsonResponse implements CacheableResponseInterface {
+
+  use CacheableResponseTrait;
+
+}
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 48b53bd..57210c2 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -115,11 +115,19 @@ public function onRespond(FilterResponseEvent $event) {
     $response->headers->set('X-Content-Type-Options', 'nosniff', FALSE);
     $response->headers->set('X-Frame-Options', 'SAMEORIGIN', FALSE);
 
+    // If the current response isn't an implementation of the
+    // CacheableResponseInterface, we're trusting that a Response is either
+    // explicitly not cacheable or that caching headers are already set from
+    // another place.
+    if (!($response instanceof CacheableResponseInterface)) {
+      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, when
     // either a reverse proxy is being used (so the reverse proxy or CDN can be
     // invalidated when appropriate) or when developing/debugging.
-    if ((Settings::get('reverse_proxy', FALSE) || Settings::get('send_cacheability_headers', FALSE)) && $response instanceof CacheableResponseInterface) {
+    if (Settings::get('reverse_proxy', FALSE) || Settings::get('send_cacheability_headers', FALSE)) {
       $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())));
diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php
index e97a1b4..e00b947 100644
--- a/core/modules/page_cache/src/Tests/PageCacheTest.php
+++ b/core/modules/page_cache/src/Tests/PageCacheTest.php
@@ -406,4 +406,50 @@ public function testFormImmutability() {
     $this->assertText("Immutable: FALSE", "Form is not immutable,");
   }
 
+  /**
+   * Tests cacheability of a CacheableResponse
+   *
+   * Tests the difference between having a controller return a normal Response
+   * object versus returning a Response object that implements the
+   * CacheableResponseInterface.
+   */
+  public function testCacheableResponseResponses() {
+    $config = $this->config('system.performance');
+    $config->set('cache.page.max_age', 300);
+    $config->save();
+
+    // 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->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->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-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.');
+
+    // Should be cached now.
+    $this->drupalGet('/system-test/respond-cacheable-reponse');
+    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
+    $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.');
+
+    // Uninstall page cache. This should flush all caches so the next call to a
+    // previously cache page should be a miss now.
+    $this->container->get('module_installer')
+      ->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');
+  }
+
 }
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index ee4b890..78b9ecb 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\rest;
 
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
@@ -67,7 +68,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
         catch (UnexpectedValueException $e) {
           $error['error'] = $e->getMessage();
           $content = $serializer->serialize($error, $format);
-          return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
+          return new CacheableResponse($content, 400, array('Content-Type' => $request->getMimeType($format)));
         }
       }
       else {
@@ -100,7 +101,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
       // Add the default content type, but only if the headers from the
       // exception have not specified it already.
       $headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format));
-      return new Response($content, $e->getStatusCode(), $headers);
+      return new CacheableResponse($content, $e->getStatusCode(), $headers);
     }
 
     // Serialize the outgoing data for the response, if available.
diff --git a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php
index c24d7c5..a77c472 100644
--- a/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php
+++ b/core/modules/system/tests/modules/system_test/src/Controller/PageCacheAcceptHeaderController.php
@@ -7,9 +7,9 @@
 
 namespace Drupal\system_test\Controller;
 
+use Drupal\Core\Cache\CacheableJsonResponse;
+use Drupal\Core\Cache\CacheableResponse;
 use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\JsonResponse;
-use Symfony\Component\HttpFoundation\Response;
 
 /**
  * Defines a controller to respond the page cache accept header test.
@@ -26,10 +26,10 @@ class PageCacheAcceptHeaderController {
    */
   public function content(Request $request) {
     if ($request->getRequestFormat() === 'json') {
-      return new JsonResponse(array('content' => 'oh hai this is json'));
+      return new CacheableJsonResponse(['content' => 'oh hai this is json']);
     }
     else {
-      return new Response("<p>oh hai this is html.</p>");
+      return new CacheableResponse("<p>oh hai this is html.</p>");
     }
   }
 }
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 5ac7ca0..9847adf 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
@@ -8,6 +8,7 @@
 namespace Drupal\system_test\Controller;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -241,13 +242,21 @@ public function authorizeInit($page_title) {
    */
   public function setHeader(Request $request) {
     $query = $request->query->all();
-    $response = new Response();
+    $response = new CacheableResponse();
     $response->headers->set($query['name'], $query['value']);
     $response->setContent($this->t('The following header was set: %name: %value', array('%name' => $query['name'], '%value' => $query['value'])));
 
     return $response;
   }
 
+  public function respondWithReponse(Request $request) {
+    return new Response('test');
+  }
+
+  public function respondWithCacheableReponse(Request $request) {
+    return new CacheableResponse('test');
+  }
+
   /**
    * A simple page callback which adds a register shutdown function.
    */
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 ecb9921..94a1bef 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
@@ -129,3 +129,17 @@ system_test.permission_dependent_route_access:
     _controller: '\Drupal\system_test\Controller\SystemTestController::mainContentFallback'
   requirements:
     _permission: 'pet llamas'
+
+system_test.respond_response:
+  path: '/system-test/respond-reponse'
+  defaults:
+    _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithReponse'
+  requirements:
+    _access: 'TRUE'
+
+system_test.respond_cacheable_response:
+  path: '/system-test/respond-cacheable-reponse'
+  defaults:
+    _controller: '\Drupal\system_test\Controller\SystemTestController::respondWithCacheableReponse'
+  requirements:
+    _access: 'TRUE'
