diff --git a/core/modules/rest/src/Plugin/views/display/RestExport.php b/core/modules/rest/src/Plugin/views/display/RestExport.php
index 1eba5a9..006871c 100644
--- a/core/modules/rest/src/Plugin/views/display/RestExport.php
+++ b/core/modules/rest/src/Plugin/views/display/RestExport.php
@@ -8,8 +8,12 @@
 namespace Drupal\rest\Plugin\views\display;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\Routing\RouteProviderInterface;
+use Drupal\Tests\Core\Cache\CacheTagsInvalidatorTest;
 use Drupal\views\ViewExecutable;
 use Drupal\views\Plugin\views\display\PathPluginBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -259,7 +263,21 @@ public function execute() {
     parent::execute();
 
     $output = $this->view->render();
-    return new Response(drupal_render_root($output), 200, array('Content-type' => $this->getMimeType()));
+
+    $header = [];
+    $header['Content-type'] = $this->getMimeType();
+
+    $response = new CacheableResponse($this->renderer->renderRoot($output), 200);
+
+    $cache_metadata = new CacheableMetadata();
+
+    /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
+    $cache = $this->getPlugin('cache');
+    $cache_metadata->setCacheTags($cache->getCacheTags());
+    $cache_metadata->setCacheMaxAge($cache->getCacheMaxAge());
+    $response->addCacheableDependency($cache_metadata);
+
+    return $response;
   }
 
   /**
diff --git a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
index 12eb653..fd01c78 100644
--- a/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
+++ b/core/modules/rest/src/Tests/Views/StyleSerializerTest.php
@@ -8,6 +8,9 @@
 namespace Drupal\rest\Tests\Views;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
+use Drupal\page_cache\Tests\PageCacheTagsIntegrationTest;
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\views\Views;
 use Drupal\views\Tests\Plugin\PluginTestBase;
 use Drupal\views\Tests\ViewTestData;
@@ -24,6 +27,13 @@
  */
 class StyleSerializerTest extends PluginTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $dumpHeaders = TRUE;
+
   /**
    * Modules to install.
    *
@@ -69,6 +79,7 @@ public function testSerializerResponses() {
 
     $actual_json = $this->drupalGet('test/serialize/field', array(), array('Accept: application/json'));
     $this->assertResponse(200);
+    $this->assertCacheTags($view->getCacheTags());
 
     // Test the http Content-type.
     $headers = $this->drupalGetHeaders();
@@ -117,16 +128,24 @@ public function testSerializerResponses() {
     $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/json'));
     $this->assertResponse(200);
     $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
+    $expected_cache_tags = $view->getCacheTags();
+    $expected_cache_tags[] = 'entity_test_list';
+    /** @var \Drupal\Core\Entity\EntityInterface $entity */
+    foreach ($entities as $entity) {
+      $expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags());
+    }
+    $this->assertCacheTags($expected_cache_tags);
 
     $expected = $serializer->serialize($entities, 'hal_json');
     $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/hal+json'));
     $this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
+    $this->assertCacheTags($expected_cache_tags);
   }
 
   /**
    * Tests the response format configuration.
    */
-  public function testReponseFormatConfiguration() {
+  public function ptestReponseFormatConfiguration() {
     $this->drupalLogin($this->adminUser);
 
     $style_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/style_options';
@@ -169,7 +188,7 @@ public function testReponseFormatConfiguration() {
   /**
    * Test the field ID alias functionality of the DataFieldRow plugin.
    */
-  public function testUIFieldAlias() {
+  public function ptestUIFieldAlias() {
     $this->drupalLogin($this->adminUser);
 
     // Test the UI settings for adding field ID aliases.
@@ -236,7 +255,7 @@ public function testUIFieldAlias() {
   /**
    * Tests the raw output options for row field rendering.
    */
-  public function testFieldRawOutput() {
+  public function ptestFieldRawOutput() {
     $this->drupalLogin($this->adminUser);
 
     // Test the UI settings for adding field ID aliases.
@@ -262,7 +281,7 @@ public function testFieldRawOutput() {
   /**
    * Tests the live preview output for json output.
    */
-  public function testLivePreview() {
+  public function ptestLivePreview() {
     // We set up a request so it looks like an request in the live preview.
     $request = new Request();
     $request->setFormat('drupal_ajax', 'application/vnd.drupal-ajax');
@@ -295,7 +314,7 @@ public function testLivePreview() {
   /**
    * Tests the views interface for rest export displays.
    */
-  public function testSerializerViewsUI() {
+  public function ptestSerializerViewsUI() {
     $this->drupalLogin($this->adminUser);
     // Click the "Update preview button".
     $this->drupalPostForm('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1', $edit = array(), t('Update preview'));
@@ -308,7 +327,7 @@ public function testSerializerViewsUI() {
   /**
    * Tests the field row style using fieldapi fields.
    */
-  public function testFieldapiField() {
+  public function ptestFieldapiField() {
     $this->drupalCreateContentType(array('type' => 'page'));
     $node = $this->drupalCreateNode();
 
diff --git a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
index 43e24ec..f1c34af 100644
--- a/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
+++ b/core/modules/system/src/Tests/Cache/AssertPageCacheContextsAndTagsTrait.php
@@ -25,6 +25,16 @@ protected function enablePageCaching() {
     $config->save();
   }
 
+  protected function getCacheHeaderValues($header_name) {
+    $header_value = $this->drupalGetHeader($header_name);
+    if (empty($header_value)) {
+      return [];
+    }
+    else {
+      return explode(' ', $header_value);
+    }
+  }
+
   /**
    * Asserts page cache miss, then hit for the given URL; checks cache headers.
    *
@@ -40,47 +50,16 @@ protected function assertPageCacheContextsAndTags(Url $url, array $expected_cont
     sort($expected_contexts);
     sort($expected_tags);
 
-    $get_cache_header_values = function ($header_name) {
-      $header_value = $this->drupalGetHeader($header_name);
-      if (empty($header_value)) {
-        return [];
-      }
-      else {
-        return explode(' ', $header_value);
-      }
-    };
-
     // Assert cache miss + expected cache contexts + tags.
     $this->drupalGet($absolute_url);
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS');
-    $actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
-    $actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
-    $this->assertIdentical($actual_contexts, $expected_contexts);
-    if ($actual_contexts !== $expected_contexts) {
-      debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
-      debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
-    }
-    $this->assertIdentical($actual_tags, $expected_tags);
-    if ($actual_tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
-    }
+    $this->assertCacheTags($expected_tags);
+    $this->assertCacheContexts($expected_contexts);
 
     // Assert cache hit + expected cache contexts + tags.
     $this->drupalGet($absolute_url);
-    $actual_contexts = $get_cache_header_values('X-Drupal-Cache-Contexts');
-    $actual_tags = $get_cache_header_values('X-Drupal-Cache-Tags');
-    $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT');
-    $this->assertIdentical($actual_contexts, $expected_contexts);
-    if ($actual_contexts !== $expected_contexts) {
-      debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
-      debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
-    }
-    $this->assertIdentical($actual_tags, $expected_tags);
-    if ($actual_tags !== $expected_tags) {
-      debug('Missing cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
-      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
-    }
+    $this->assertCacheTags($expected_tags);
+    $this->assertCacheContexts($expected_contexts);
 
     // Assert page cache item + expected cache tags.
     $cid_parts = array($url->setAbsolute()->toString(), 'html');
@@ -94,4 +73,35 @@ protected function assertPageCacheContextsAndTags(Url $url, array $expected_cont
     }
   }
 
+  protected function assertCacheTags(array $expected_tags) {
+    $actual_tags = $this->getCacheHeaderValues('X-Drupal-Cache-Tags');
+    $this->assertIdentical($actual_tags, $expected_tags);
+    if ($actual_tags !== $expected_tags) {
+      debug('Missing cache tags: ' . implode(',', array_diff($actual_tags, $expected_tags)));
+      debug('Unwanted cache tags: ' . implode(',', array_diff($expected_tags, $actual_tags)));
+    }
+  }
+
+  protected function assertCacheContexts(array $expected_contexts) {
+    $actual_contexts = $this->getCacheHeaderValues('X-Drupal-Cache-Contexts');
+    $this->assertIdentical($actual_contexts, $expected_contexts);
+    if ($actual_contexts !== $expected_contexts) {
+      debug('Missing cache contexts: ' . implode(',', array_diff($actual_contexts, $expected_contexts)));
+      debug('Unwanted cache contexts: ' . implode(',', array_diff($expected_contexts, $actual_contexts)));
+    }
+  }
+
+  /**
+   * Asserts the max age header.
+   *
+   * @param int $max_age
+   */
+  protected function assertCacheMaxAge($max_age) {
+    $cache_control_header = $this->drupalGetHeader('Cache-Control');
+    if (strpos($cache_control_header, 'max-age:' . $max_age) === FALSE) {
+      debug('Expected max_age:' . $max_age . '; Response max_age:' . $cache_control_header);
+    }
+    $this->assertTrue(strpos($cache_control_header, 'max-age:' . $max_age));
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
index 224a964..d850d4f 100644
--- a/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
+++ b/core/modules/views/src/Plugin/views/cache/CachePluginBase.php
@@ -373,6 +373,25 @@ public function getCacheTags() {
   }
 
   /**
+   * Gets the max age for the current view.
+   *
+   * @return int
+   */
+  public function getCacheMaxAge() {
+    $max_age = $this->getDefaultCacheMaxAge();
+    $max_age = Cache::mergeMaxAges($max_age, $this->view->getQuery()->getCacheMaxAges());
+    return $max_age;
+  }
+
+  /**
+   * Returns the default cache max age.
+   */
+  protected function getDefaultCacheMaxAge() {
+    // The default cache backend is not caching anything.
+    return 0;
+  }
+
+  /**
    * Prepares the view result before putting it into cache.
    *
    * @param \Drupal\views\ResultRow[] $result
diff --git a/core/modules/views/src/Plugin/views/cache/Tag.php b/core/modules/views/src/Plugin/views/cache/Tag.php
index 144709d..20d97c3 100644
--- a/core/modules/views/src/Plugin/views/cache/Tag.php
+++ b/core/modules/views/src/Plugin/views/cache/Tag.php
@@ -6,6 +6,7 @@
  */
 
 namespace Drupal\views\Plugin\views\cache;
+use Drupal\Core\Cache\CacheBackendInterface;
 
 /**
  * Simple caching of query results for Views displays.
@@ -34,4 +35,11 @@ protected function cacheExpire($type) {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultCacheMaxAge() {
+    return CacheBackendInterface::CACHE_PERMANENT;
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/cache/Time.php b/core/modules/views/src/Plugin/views/cache/Time.php
index 1aff4f0..f8ef786 100644
--- a/core/modules/views/src/Plugin/views/cache/Time.php
+++ b/core/modules/views/src/Plugin/views/cache/Time.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Render\RendererInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Simple caching of query results for Views displays.
@@ -40,6 +41,13 @@ class Time extends CachePluginBase {
   protected $dateFormatter;
 
   /**
+   * The current request.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
    * Constructs a Time cache plugin object.
    *
    * @param array $configuration
@@ -54,9 +62,13 @@ class Time extends CachePluginBase {
    *   The date formatter service.
    * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
    *   The render cache service.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, RenderCacheInterface $render_cache, DateFormatter $date_formatter) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, RendererInterface $renderer, RenderCacheInterface $render_cache, DateFormatter $date_formatter, Request $request) {
     $this->dateFormatter = $date_formatter;
+    $this->request = $request;
+
     parent::__construct($configuration, $plugin_id, $plugin_definition, $renderer, $render_cache);
   }
 
@@ -70,7 +82,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_definition,
       $container->get('renderer'),
       $container->get('render_cache'),
-      $container->get('date.formatter')
+      $container->get('date.formatter'),
+      $container->get('request_stack')->getCurrentRequest()
     );
   }
 
@@ -174,4 +187,11 @@ protected function cacheSetExpire($type) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDefaultCacheMaxAge() {
+    return $this->cacheSetExpire('output');
+  }
+
 }
diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
index 2b2b4a2..f751a63 100644
--- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
+++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\views\Plugin\views\query;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\views\Plugin\CacheablePluginInterface;
 use Drupal\views\Plugin\views\PluginBase;
@@ -341,6 +342,10 @@ public function getCacheTags() {
     return [];
   }
 
+  public function getCacheMaxAges() {
+    return Cache::PERMANENT;
+  }
+
 }
 
 /**
diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php
index 660caf1..6d98239 100644
--- a/core/modules/views/src/Plugin/views/query/Sql.php
+++ b/core/modules/views/src/Plugin/views/query/Sql.php
@@ -1552,16 +1552,45 @@ public function getCacheTags() {
     $tags = [];
     // Add cache tags for each row, if there is an entity associated with it.
     if (!$this->hasAggregate) {
-      foreach ($this->view->result as $row)  {
-        if ($row->_entity) {
-          $tags = Cache::mergeTags($row->_entity->getCacheTags(), $tags);
-        }
+      foreach ($this->getAllEntities() as $entity) {
+        $tags = Cache::mergeTags($entity->getCacheTags(), $tags);
       }
     }
 
     return $tags;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAges() {
+    $max_age = parent::getCacheMaxAges();
+    foreach ($this->getAllEntities() as $entity) {
+      $max_age = Cache::mergeMaxAges($max_age, $entity->getCacheMaxAge());
+    }
+
+    return $max_age;
+  }
+
+  /**
+   * Gets all the involved entities of the view.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   */
+  protected function getAllEntities() {
+    $entities = [];
+    foreach ($this->view->result as $row) {
+      if ($row->_entity) {
+        $entities[] = $row->_entity;
+      }
+      foreach ($row->_relationship_entities as $entity) {
+        $entities[] = $entity;
+      }
+    }
+
+    return $entities;
+  }
+
   public function addSignature(ViewExecutable $view) {
     $view->query->addField(NULL, "'" . $view->storage->id() . ':' . $view->current_display . "'", 'view_name');
   }
diff --git a/core/modules/views/src/Tests/Plugin/CacheWebTest.php b/core/modules/views/src/Tests/Plugin/CacheWebTest.php
index 3c825a7..b3a01e2 100644
--- a/core/modules/views/src/Tests/Plugin/CacheWebTest.php
+++ b/core/modules/views/src/Tests/Plugin/CacheWebTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\views\Tests\Plugin;
 
+use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\views\Views;
 
 /**
@@ -17,6 +18,8 @@
  */
 class CacheWebTest extends PluginTestBase {
 
+  use AssertPageCacheContextsAndTagsTrait;
+
   /**
    * Views used by this test.
    *
@@ -63,10 +66,12 @@ public function testCacheOutputOnPage() {
     $this->drupalGet('test-display');
     $this->assertResponse(200);
     $this->assertTrue(\Drupal::cache('render')->get($output_key));
+    $this->assertCacheMaxAge(time() + 3600);
 
     $this->drupalGet('test-display');
     $this->assertResponse(200);
     $this->assertTrue(\Drupal::cache('render')->get($output_key));
+    $this->assertCacheMaxAge(time() + 3600);
   }
 
 }
diff --git a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php
new file mode 100644
index 0000000..9c8846a
--- /dev/null
+++ b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\views\Unit\Plugin\query\SqlTest.
+ */
+
+namespace Drupal\Tests\views\Unit\Plugin\query;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\views\Plugin\views\query\Sql;
+use Drupal\views\ResultRow;
+
+/**
+ * @coversDefaultClass \Drupal\views\Plugin\views\query\Sql
+ *
+ * @group views
+ */
+class SqlTest extends UnitTestCase {
+
+  /**
+   * @covers ::getCacheTags
+   * @covers ::getAllEntities
+   */
+  public function testGetCacheTags() {
+    $view = $this->getMockBuilder('Drupal\views\ViewExecutable')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $query = new Sql([], 'sql', []);
+    $query->view = $view;
+
+    $view->result = [];
+
+    // Add a row with an entity.
+    $row = new ResultRow();
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheTags')
+      ->willReturn(['entity_test:123']);
+
+    $row->_entity = $entity;
+    $view->result[] = $row;
+
+    // Add a row with an entity and a relationship entity.
+    $row = new ResultRow();
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheTags')
+      ->willReturn(['entity_test:124']);
+    $row->_entity = $entity;
+
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheTags')
+      ->willReturn(['entity_test:125']);
+    $row->_relationship_entities[] = $entity;
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheTags')
+      ->willReturn(['entity_test:126']);
+    $row->_relationship_entities[] = $entity;
+
+    $view->result[] = $row;
+
+    $this->assertEquals(['entity_test:123', 'entity_test:124', 'entity_test:125', 'entity_test:126'], $query->getCacheTags());
+  }
+
+  /**
+   * @covers ::getCacheTags
+   * @covers ::getAllEntities
+   */
+  public function testGetCacheMaxAge() {
+    $view = $this->getMockBuilder('Drupal\views\ViewExecutable')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $query = new Sql([], 'sql', []);
+    $query->view = $view;
+
+    $view->result = [];
+
+    // Add a row with an entity.
+    $row = new ResultRow();
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheMaxAge')
+      ->willReturn(10);
+
+    $row->_entity = $entity;
+    $view->result[] = $row;
+
+    // Add a row with an entity and a relationship entity.
+    $row = new ResultRow();
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheMaxAge')
+      ->willReturn(20);
+    $row->_entity = $entity;
+
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheMaxAge')
+      ->willReturn(30);
+    $row->_relationship_entities[] = $entity;
+    $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+    $entity->expects($this->any())
+      ->method('getCacheMaxAge')
+      ->willReturn(40);
+    $row->_relationship_entities[] = $entity;
+
+    $this->assertEquals(10, $query->getCacheMaxAges());
+  }
+
+}
