diff --git a/core/authorize.php b/core/authorize.php
index 97fbe24..b7208e7 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -46,6 +46,7 @@
  *   TRUE if the current user can run authorize.php, and FALSE if not.
  */
 function authorize_access_allowed() {
+  require_once DRUPAL_ROOT . '/core/includes/database.inc';
   require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc');
   drupal_session_initialize();
   return Settings::get('allow_authorize_operations', TRUE) && user_access('administer software updates');
@@ -55,12 +56,13 @@ function authorize_access_allowed() {
 
 require_once __DIR__ . '/includes/bootstrap.inc';
 require_once __DIR__ . '/includes/common.inc';
+require_once __DIR__ . '/includes/database.inc';
 require_once __DIR__ . '/includes/file.inc';
 require_once __DIR__ . '/includes/module.inc';
 require_once __DIR__ . '/includes/ajax.inc';
 
 // Prepare a minimal bootstrap.
-drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 $request = \Drupal::request();
 
 // We have to enable the user and system modules, even to check access and
diff --git a/core/core.services.yml b/core/core.services.yml
index 90c40a9..b4c9226 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -67,6 +67,42 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [data]
+  cache.path:
+    class: Drupal\Core\Cache\CacheBackendInterface
+    tags:
+      - { name: cache.bin }
+    factory_method: get
+    factory_service: cache_factory
+    arguments: [path]
+  page_cache_request_policy:
+    class: Drupal\Core\PageCache\RequestPolicy
+    factory_class: Drupal\Core\PageCache\RequestPolicy
+    factory_method: createDefault
+    tags:
+      - { name: page_cache_request_policy }
+  page_cache_response_policy:
+    class: Drupal\Core\PageCache\ResponsePolicy
+    factory_class: Drupal\Core\PageCache\ResponsePolicy
+    factory_method: createDefault
+    tags:
+      - { name: page_cache_response_policy }
+  page_cache_kill_switch:
+    class: Drupal\Core\PageCache\ResponsePolicyRule\KillSwitch
+    tags:
+      - { name: page_cache_response_policy.deny }
+  page_cache_response_subscriber:
+    class: Drupal\Core\PageCache\DefaultCacheControlResponseSubscriber
+    arguments: ['@page_cache_request_policy', '@page_cache_response_policy', '@config.factory', '@settings']
+    tags:
+      - { name: event_subscriber }
+  page_cache_cid_generator:
+    class: Drupal\Core\PageCache\Storage\CidGenerator
+    arguments: ['@content_negotiation']
+  page_cache_storage:
+    class: Drupal\Core\PageCache\Storage\InternalCache
+    factory_class: Drupal\Core\PageCache\Storage\InternalCache
+    factory_method: createFromConfig
+    arguments: ['@cache.render', '@page_cache_cid_generator', '@config.factory']
   config.cachedstorage.storage:
     class: Drupal\Core\Config\FileStorage
     factory_class: Drupal\Core\Config\FileStorageFactory
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index a704ebf..e60d81c 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -11,8 +11,9 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Extension\ExtensionDiscovery;
-use Drupal\Core\Utility\Title;
+use Drupal\Core\PageCache\PageCacheFactory;
 use Drupal\Core\Utility\Error;
+use Drupal\Core\Utility\Title;
 use Symfony\Component\ClassLoader\ApcClassLoader;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\Container;
@@ -135,19 +136,14 @@
 const DRUPAL_BOOTSTRAP_KERNEL = 1;
 
 /**
- * Third bootstrap phase: try to serve a cached page.
- */
-const DRUPAL_BOOTSTRAP_PAGE_CACHE = 2;
-
-/**
  * Fourth bootstrap phase: load code for subsystems and modules.
  */
-const DRUPAL_BOOTSTRAP_CODE = 3;
+const DRUPAL_BOOTSTRAP_CODE = 2;
 
 /**
  * Final bootstrap phase: initialize language, path, theme, and modules.
  */
-const DRUPAL_BOOTSTRAP_FULL = 4;
+const DRUPAL_BOOTSTRAP_FULL = 3;
 
 /**
  * Role ID for anonymous users; should match what's in the "role" table.
@@ -622,62 +618,6 @@ function drupal_get_filename($type, $name, $filename = NULL) {
 }
 
 /**
- * Gets the page cache cid for this request.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return string
- *   The cid for this request.
- */
-function drupal_page_cache_get_cid(Request $request) {
-  $cid_parts = array(
-    $request->getUri(),
-    \Drupal::service('content_negotiation')->getContentType($request),
-  );
-  return sha1(implode(':', $cid_parts));
-}
-
-/**
- * Retrieves the current page from the cache.
- *
- * Note: we do not serve cached pages to authenticated users, or to anonymous
- * users when $_SESSION is non-empty. $_SESSION may contain status messages
- * from a form submission, the contents of a shopping cart, or other user-
- * specific content that should not be cached and displayed to other users.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return
- *   The cache object, if the page was found in the cache, NULL otherwise.
- */
-function drupal_page_get_cache(Request $request) {
-  if (drupal_page_is_cacheable()) {
-    return \Drupal::cache('render')->get(drupal_page_cache_get_cid($request));
-  }
-}
-
-/**
- * Determines the cacheability of the current page.
- *
- * @param $allow_caching
- *   Set to FALSE if you want to prevent this page to get cached.
- *
- * @return
- *   TRUE if the current page can be cached, FALSE otherwise.
- */
-function drupal_page_is_cacheable($allow_caching = NULL) {
-  $allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
-  if (isset($allow_caching)) {
-    $allow_caching_static = $allow_caching;
-  }
-
-  return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
-    && !drupal_is_cli();
-}
-
-/**
  * Sets an HTTP response header for the current page.
  *
  * Note: When sending a Content-Type header, always include a 'charset' type,
@@ -849,113 +789,6 @@ function drupal_page_header() {
 }
 
 /**
- * Sets HTTP headers in preparation for a cached page response.
- *
- * The headers allow as much as possible in proxies and browsers without any
- * particular knowledge about the pages. Modules can override these headers
- * using drupal_add_http_header().
- *
- * If the request is conditional (using If-Modified-Since and If-None-Match),
- * and the conditions match those currently in the cache, a 304 Not Modified
- * response is sent.
- */
-function drupal_serve_page_from_cache(stdClass $cache, Response $response, Request $request) {
-  $config = \Drupal::config('system.performance');
-
-  // First half: we must determine if we should be returning a 304.
-
-  // Negotiate whether to use compression.
-  $page_compression = !empty($cache->data['page_compressed']) && extension_loaded('zlib');
-  $return_compressed = $page_compression && $request->server->has('HTTP_ACCEPT_ENCODING') && strpos($request->server->get('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE;
-
-  // Get headers. Keys are lower-case.
-  $boot_headers = drupal_get_http_header();
-
-  foreach ($cache->data['headers'] as $name => $value) {
-    // In the case of a 304 response, certain headers must be sent, and the
-    // remaining may not (see RFC 2616, section 10.3.5).
-    $name_lower = strtolower($name);
-    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
-      $response->headers->set($name, $value);
-      unset($cache->data['headers'][$name]);
-    }
-  }
-
-  // If the client sent a session cookie, a cached copy will only be served
-  // to that one particular client due to Vary: Cookie. Thus, do not set
-  // max-age > 0, allowing the page to be cached by external proxies, when a
-  // session cookie is present unless the Vary header has been replaced.
-  $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
-  // RFC 2616, section 14.21 says: 'To mark a response as "never expires," an
-  // origin server sends an Expires date approximately one year from the time
-  // the response is sent. HTTP/1.1 servers SHOULD NOT send Expires dates more
-  // than one year in the future.'
-  if ($max_age > 31536000 || $max_age === \Drupal\Core\Cache\Cache::PERMANENT) {
-    $max_age = 31536000;
-  }
-  $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
-
-  // Entity tag should change if the output changes.
-  $response->setEtag($cache->created);
-
-  // See if the client has provided the required HTTP headers.
-  $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
-  $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
-
-  if ($if_modified_since && $if_none_match
-      && $if_none_match == $response->headers->get('etag') // etag must match
-      && $if_modified_since == $cache->created) {  // if-modified-since must match
-    $response->setStatusCode(304);
-    return;
-  }
-
-  // Second half: we're not returning a 304, so put in other headers.
-
-  // Send the remaining headers.
-  foreach ($cache->data['headers'] as $name => $value) {
-    $response->headers->set($name, $value);
-    drupal_add_http_header($name, $value);
-  }
-
-  $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
-
-  // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
-  // by sending an Expires date in the past. HTTP/1.1 clients ignores the
-  // Expires header if a Cache-Control: max-age= directive is specified (see RFC
-  // 2616, section 14.9.3).
-  if (!$response->getExpires()) {
-    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-  }
-
-  // Allow HTTP proxies to cache pages for anonymous users without a session
-  // cookie. The Vary header is used to indicates the set of request-header
-  // fields that fully determines whether a cache is permitted to use the
-  // response to reply to a subsequent request for a given URL without
-  // revalidation.
-  if (!isset($boot_headers['vary']) && !Settings::get('omit_vary_cookie')) {
-    $response->setVary('Cookie', FALSE);
-  }
-
-  if ($page_compression) {
-    $response->setVary('accept-encoding', FALSE);
-    // If page_compression is enabled, the cache contains gzipped data.
-    if ($return_compressed) {
-      // $cache->data['body'] is already gzip'ed, so make sure
-      // zlib.output_compression does not compress it once more.
-      ini_set('zlib.output_compression', '0');
-      $response->headers->set('content-encoding', 'gzip');
-    }
-    else {
-      // The client does not support compression, so unzip the data in the
-      // cache. Strip the gzip header and run uncompress.
-      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
-    }
-  }
-
-  $response->setContent($cache->data['body']);
-}
-
-/**
  * Translates a string to the current language or to a given language.
  *
  * The t() function serves two purposes. First, at run-time it translates
@@ -1266,7 +1099,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
     }
 
     // Mark this page as being uncacheable.
-    drupal_page_is_cacheable(FALSE);
+    \Drupal::service('page_cache_kill_switch')->trigger();
   }
 
   // Messages not set when DB connection fails.
@@ -1348,7 +1181,6 @@ function drupal_anonymous_user() {
  *   values:
  *   - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
  *   - DRUPAL_BOOTSTRAP_KERNEL: Initalizes a kernel.
- *   - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
  *   - DRUPAL_BOOTSTRAP_CODE: Loads code for subsystems and modules.
  *   - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
  *     data.
@@ -1361,7 +1193,6 @@ function drupal_bootstrap($phase = NULL) {
   static $phases = array(
     DRUPAL_BOOTSTRAP_CONFIGURATION,
     DRUPAL_BOOTSTRAP_KERNEL,
-    DRUPAL_BOOTSTRAP_PAGE_CACHE,
     DRUPAL_BOOTSTRAP_CODE,
     DRUPAL_BOOTSTRAP_FULL,
   );
@@ -1399,10 +1230,6 @@ function drupal_bootstrap($phase = NULL) {
           _drupal_bootstrap_kernel();
           break;
 
-        case DRUPAL_BOOTSTRAP_PAGE_CACHE:
-          _drupal_bootstrap_page_cache();
-          break;
-
         case DRUPAL_BOOTSTRAP_CODE:
           require_once __DIR__ . '/common.inc';
           _drupal_bootstrap_code();
@@ -1449,18 +1276,11 @@ function drupal_handle_request($test_only = FALSE) {
   }
 
   $kernel = new DrupalKernel('prod', drupal_classloader(), !$test_only);
+  $kernel = PageCacheFactory::createFromSettings($kernel, Settings::getInstance());
 
-  // @todo Remove this once everything in the bootstrap has been
-  //   converted to services in the DIC.
-  $kernel->boot();
-
-  // Create a request object from the HttpFoundation.
+  // Create a request object and pass it to the kernel.
   $request = Request::createFromGlobals();
-  \Drupal::getContainer()->set('request', $request);
-
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
-
-  $response = $kernel->handle($request)->prepare($request)->send();
+  $response = $kernel->handle($request)->send();
 
   $kernel->terminate($request, $response);
 }
@@ -1611,49 +1431,6 @@ function _drupal_bootstrap_kernel() {
 }
 
 /**
- * Attempts to serve a page from the cache.
- */
-function _drupal_bootstrap_page_cache() {
-  global $user;
-
-  require_once __DIR__ . '/database.inc';
-  // Check for a cache mode force from settings.php.
-  if (Settings::get('page_cache_without_database')) {
-    $cache_enabled = TRUE;
-  }
-  else {
-    $config = \Drupal::config('system.performance');
-    $cache_enabled = $config->get('cache.page.use_internal');
-  }
-
-  $request = Request::createFromGlobals();
-  // If there is no session cookie and cache is enabled (or forced), try
-  // to serve a cached page.
-  if (!$request->cookies->has(session_name()) && $cache_enabled) {
-    // Make sure there is a user object because its timestamp will be checked.
-    $user = new AnonymousUserSession();
-    // Get the page from the cache.
-    $cache = drupal_page_get_cache($request);
-    // If there is a cached page, display it.
-    if (is_object($cache)) {
-      $response = new Response();
-      $response->headers->set('X-Drupal-Cache', 'HIT');
-      date_default_timezone_set(drupal_get_user_timezone());
-
-      drupal_serve_page_from_cache($cache, $response, $request);
-
-      // We are done.
-      $response->prepare($request);
-      $response->send();
-      exit;
-    }
-    else {
-      drupal_add_http_header('X-Drupal-Cache', 'MISS');
-    }
-  }
-}
-
-/**
  * Returns the current bootstrap phase for this Drupal process.
  *
  * The current phase is the one most recently completed by drupal_bootstrap().
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 44cafd1..9f3447b 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -18,7 +18,6 @@
 use Drupal\Component\PhpStorage\PhpStorageFactory;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Datetime\DrupalDateTime;
-use Drupal\Core\EventSubscriber\HtmlViewSubscriber;
 use Drupal\Core\Routing\GeneratorNotInitializedException;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Render\Element;
@@ -2904,6 +2903,7 @@ function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
  * Loads code for subsystems and modules, and registers stream wrappers.
  */
 function _drupal_bootstrap_code() {
+  require_once __DIR__ . '/database.inc';
   require_once __DIR__ . '/../../' . Settings::get('path_inc', 'core/includes/path.inc');
   require_once __DIR__ . '/module.inc';
   require_once __DIR__ . '/theme.inc';
@@ -2964,68 +2964,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
 }
 
 /**
- * Stores the current page in the cache.
- *
- * If page_compression is enabled, a gzipped version of the page is stored in
- * the cache to avoid compressing the output on each request. The cache entry
- * is unzipped in the relatively rare event that the page is requested by a
- * client without gzip support.
- *
- * Page compression requires the PHP zlib extension
- * (http://php.net/manual/ref.zlib.php).
- *
- * @param $body
- *   The response body.
- * @return
- *   The cached object or NULL if the page cache was not set.
- *
- * @see drupal_page_header()
- */
-function drupal_page_set_cache(Response $response, Request $request) {
-  if (drupal_page_is_cacheable()) {
-
-    // Check if the current page may be compressed.
-    $page_compressed = \Drupal::config('system.performance')->get('response.gzip') && extension_loaded('zlib');
-
-    $cache = (object) array(
-      'cid' => drupal_page_cache_get_cid($request),
-      'data' => array(
-        'body' => $response->getContent(),
-        'headers' => array(),
-        // We need to store whether page was compressed or not,
-        // because by the time it is read, the configuration might change.
-        'page_compressed' => $page_compressed,
-      ),
-      'tags' => HtmlViewSubscriber::convertHeaderToCacheTags($response->headers->get('X-Drupal-Cache-Tags')),
-      'expire' => Cache::PERMANENT,
-      'created' => REQUEST_TIME,
-    );
-
-    $cache->data['headers'] = $response->headers->all();
-
-    // Hack: exclude the x-drupal-cache header; it may make it in here because
-    // of awkwardness in how we defer sending it over in _drupal_page_get_cache.
-    if (isset($cache->data['headers']['x-drupal-cache'])) {
-      unset($cache->data['headers']['x-drupal-cache']);
-    }
-
-     // Use the actual timestamp from an Expires header, if available.
-    if ($date = $response->getExpires()) {
-      $date = DrupalDateTime::createFromDateTime($date);
-      $cache->expire = $date->getTimestamp();
-    }
-
-    if ($cache->data['body']) {
-      if ($page_compressed) {
-        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
-      }
-      \Drupal::cache('render')->set($cache->cid, $cache->data, $cache->expire, $cache->tags);
-    }
-    return $cache;
-  }
-}
-
-/**
  * Sets the main page content value for later use.
  *
  * Given the nature of the Drupal page handling, this will be called once with
diff --git a/core/includes/session.inc b/core/includes/session.inc
index 76aa83b..02a7e61 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -29,9 +29,6 @@ function drupal_session_initialize() {
     // anonymous users not use a session cookie unless something is stored in
     // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
     drupal_session_start();
-    if ($user->isAuthenticated() || !empty($_SESSION)) {
-      drupal_page_is_cacheable(FALSE);
-    }
   }
   else {
     // Set a session identifier for this request. This is necessary because
diff --git a/core/includes/utility.inc b/core/includes/utility.inc
index b18c941..f95ff3a 100644
--- a/core/includes/utility.inc
+++ b/core/includes/utility.inc
@@ -41,16 +41,12 @@ function drupal_rebuild() {
   restore_error_handler();
   restore_exception_handler();
 
-  // drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL) will build a new kernel. This
-  // comes before DRUPAL_BOOTSTRAP_PAGE_CACHE.
+  // drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL) will build a new kernel.
   PhpStorageFactory::get('service_container')->deleteAll();
   PhpStorageFactory::get('twig')->deleteAll();
 
-  // Disable the page cache.
-  drupal_page_is_cacheable(FALSE);
-
   // Bootstrap up to where caches exist and clear them.
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
   foreach (Cache::getBins() as $bin) {
     $bin->deleteAll();
   }
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 184c767..4c73007 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -11,6 +11,7 @@
 use Drupal\Component\Utility\Settings;
 use Drupal\Core\Cache\ListCacheBinsPass;
 use Drupal\Core\Config\ConfigFactoryOverridePass;
+use Drupal\Core\PageCache\BuildPolicyPass;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
@@ -75,6 +76,8 @@ public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new RegisterRouteProcessorsPass());
     $container->addCompilerPass(new ListCacheBinsPass());
     $container->addCompilerPass(new CacheContextsPass());
+    // Add the compiler pass for collecting page cache policy rules.
+    $container->addCompilerPass(new BuildPolicyPass());
     // Add the compiler pass for appending string translators.
     $container->addCompilerPass(new RegisterStringTranslatorsPass());
     // Add the compiler pass that will process the tagged breadcrumb builder
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 8e4596f..b2d3264 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -57,41 +57,6 @@ public function onRespond(FilterResponseEvent $event) {
     // Set the Content-language header.
     $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->id);
 
-    // Because pages are highly dynamic, set the last-modified time to now
-    // since the page is in fact being regenerated right now.
-    // @todo Remove this and use a more intelligent default so that HTTP
-    // caching can function properly.
-    $response->setLastModified(new \DateTime(gmdate(DATE_RFC1123, REQUEST_TIME)));
-
-    // Also give each page a unique ETag. This will force clients to include
-    // both an If-Modified-Since header and an If-None-Match header when doing
-    // conditional requests for the page (required by RFC 2616, section 13.3.4),
-    // making the validation more robust. This is a workaround for a bug in
-    // Mozilla Firefox that is triggered when Drupal's caching is enabled and
-    // the user accesses Drupal via an HTTP proxy (see
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an
-    // authenticated user requests a page, and then logs out and requests the
-    // same page again, Firefox may send a conditional request based on the
-    // page that was cached locally when the user was logged in. If this page
-    // did not have an ETag header, the request only contains an
-    // If-Modified-Since header. The date will be recent, because with
-    // authenticated users the Last-Modified header always refers to the time
-    // of the request. If the user accesses Drupal via a proxy server, and the
-    // proxy already has a cached copy of the anonymous page with an older
-    // Last-Modified date, the proxy may respond with 304 Not Modified, making
-    // the client think that the anonymous and authenticated pageviews are
-    // identical.
-    // @todo Remove this line as no longer necessary per
-    //   http://drupal.org/node/1573064
-    $response->setEtag(REQUEST_TIME);
-
-    // Authenticated users are always given a 'no-cache' header, and will fetch
-    // a fresh page on every request. This prevents authenticated users from
-    // seeing locally cached pages.
-    // @todo Revisit whether or not this is still appropriate now that the
-    //   Response object does its own cache control processing and we intend to
-    //   use partial page caching more extensively.
-
     // Attach globally-declared headers to the response object so that Symfony
     // can send them for us correctly.
     // @todo remove this once we have removed all drupal_add_http_header() calls
@@ -99,15 +64,6 @@ public function onRespond(FilterResponseEvent $event) {
     foreach ($headers as $name => $value) {
       $response->headers->set($name, $value, FALSE);
     }
-
-    $max_age = \Drupal::config('system.performance')->get('cache.page.max_age');
-    if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) {
-      drupal_serve_page_from_cache($cache, $response, $request);
-    }
-    else {
-      $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-      $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
-    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/PageCache/BuildPolicyPass.php b/core/lib/Drupal/Core/PageCache/BuildPolicyPass.php
new file mode 100644
index 0000000..6f6becc
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/BuildPolicyPass.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\BuildPolicyPass.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+
+/**
+ * Collect policy rules and attach them to the page cache policy.
+ */
+class BuildPolicyPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(ContainerBuilder $container) {
+    // Add request policy allow and deny rules to request policy service.
+    foreach ($container->findTaggedServiceIds('page_cache_request_policy') as $id => $attributes) {
+      $request_policy_def = $container->getDefinition($id);
+
+      // Register all policy rules tagged with page_cache_request_policy.deny.
+      foreach ($container->findTaggedServiceIds('page_cache_request_policy.deny') as $id => $attributes) {
+        $def = $container->getDefinition($id);
+        $class = $def->getClass();
+        $reflection_class = new \ReflectionClass($class);
+        if (!$reflection_class->implementsInterface('Drupal\Core\PageCache\RequestPolicyRuleInterface')) {
+          throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
+        }
+
+        $request_policy_def->addMethodCall('deny', array(new Reference($id)));
+      }
+
+      // Register all policy rules tagged with page_cache_request_policy.allow.
+      foreach ($container->findTaggedServiceIds('page_cache_request_policy.allow') as $id => $attributes) {
+        $def = $container->getDefinition($id);
+        $class = $def->getClass();
+        $reflection_class = new \ReflectionClass($class);
+        if (!$reflection_class->implementsInterface('Drupal\Core\PageCache\RequestPolicyRuleInterface')) {
+          throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
+        }
+
+        $request_policy_def->addMethodCall('allow', array(new Reference($id)));
+      }
+    }
+
+    // Add response policy rules to response policy service.
+    foreach ($container->findTaggedServiceIds('page_cache_response_policy') as $id => $attributes) {
+      $response_policy_def = $container->getDefinition($id);
+
+      // Register all rules tagged with page_cache_response_policy.deny.
+      foreach ($container->findTaggedServiceIds('page_cache_response_policy.deny') as $id => $attributes) {
+        $def = $container->getDefinition($id);
+        $class = $def->getClass();
+        $reflection_class = new \ReflectionClass($class);
+        if (!$reflection_class->implementsInterface('Drupal\Core\PageCache\ResponsePolicyRuleInterface')) {
+          throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
+        }
+
+        $response_policy_def->addMethodCall('deny', array(new Reference($id)));
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControlResponseSubscriberBase.php b/core/lib/Drupal/Core/PageCache/CacheControlResponseSubscriberBase.php
new file mode 100644
index 0000000..2785c01
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControlResponseSubscriberBase.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\PageCache\CacheControlResponseSubscriberBase.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Core\Config\ConfigFactory;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Response subscriber to handle finished responses.
+ */
+abstract class CacheControlResponseSubscriberBase implements EventSubscriberInterface {
+
+  /**
+   * A policy rule determining the cacheability of a request.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface
+   */
+  protected $requestPolicy;
+
+  /**
+   * A policy rule determining the cacheability of the response.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyRuleInterface
+   */
+  protected $responsePolicy;
+
+  /**
+   * The number of seconds a cache may keep and deliver a response.
+   *
+   * Do not specify a value greater than one year (31536000 seconds).
+   *
+   * @var int
+   */
+  protected $maxAge;
+
+  /**
+   * Construct Cache-Control header generator publicly cacheable responses.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyRuleInterface $response_policy
+   *   A policy rule determining the cacheability of a response.
+   * @param int $max_age
+   *   The number of seconds a cache may keep and deliver a response.
+   */
+  public function __construct(RequestPolicyRuleInterface $request_policy, ResponsePolicyRuleInterface $response_policy, $max_age) {
+    $this->requestPolicy = $request_policy;
+    $this->responsePolicy = $response_policy;
+    $this->maxAge = $max_age;
+  }
+
+  /**
+   * Set the max-age directive to be used when cache-control header is added.
+   */
+  public function setMaxAge($max_age) {
+    $this->maxAge = $max_age;
+  }
+
+  /**
+   * Return the max-age directive to be used when cache-control header is added.
+   */
+  public function getMaxAge() {
+    return $this->maxAge;
+  }
+
+  /**
+   * Determine whether the given request has a custom cache-control header.
+   *
+   * Upon construction the ResponseHeaderBag is initialized with an empty
+   * Cache-Control header. Consequently it is not possible to check whether the
+   * header was set explicitely simply by checking for the presence of it.
+   * Rather it is necessary to examine the computed Cache-Control header and
+   * match for results known to be present only when Cache-Control was never set
+   * explicitely.
+   *
+   * When neither Cache-Control nor any of the ETag, Last-Modified, Expires
+   * headers were set on the response, Cache-Control returns the value
+   * 'no-cache'. If any of ETag, Last-Modified or Expires was set but not
+   * Cache-Control, then 'private, must-revalidate' (in exactly this order) is
+   * returned.
+   *
+   * @see \Symfony\Component\HttpFoundation\ResponseHeaderBag::computeCacheControlValue()
+   *
+   * @return bool
+   *   TRUE when Cache-Control header was set explicitely on the given response.
+   */
+  protected function isCacheControlCustomized(Response $response) {
+    $cache_control = $response->headers->get('cache-control');
+    return $cache_control != 'no-cache' && $cache_control != 'private, must-revalidate';
+  }
+
+  /**
+   * Add Cache-Control and Expires header to a cacheable response.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  protected abstract function setCacheable(Response $response, Request $request);
+
+  /**
+   * Add Cache-Control and Expires header for a response which is not cacheable.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  protected function setNotCacheable(Response $response, Request $request) {
+    $this->setCacheControlNoCache($response);
+    $this->setExpiresNoCache($response);
+
+    // There is no point in sending along headers necessary for cache
+    // revalidation, if caching by proxies and browsers is denied in the first
+    // place. Therefore remove Etag, Last-Modified and Vary in that case.
+    $response->setEtag(NULL);
+    $response->setLastModified(NULL);
+    $response->setVary(NULL);
+  }
+
+  /**
+   * Disable caching in the browser and for HTTP/1.1 proxies and clients.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function setCacheControlNoCache(Response $response) {
+    $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
+  }
+
+  /**
+   * Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
+   *
+   * HTTP/1.0 proxies do not support the Vary header, so prevent any caching by
+   * sending an Expires date in the past. HTTP/1.1 clients ignore the Expires
+   * header if a Cache-Control: max-age= directive is specified (see RFC 2616,
+   * section 14.9.3).
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   */
+  protected function setExpiresNoCache(Response $response) {
+    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
+  }
+
+  /**
+   * Sets extra headers on successful responses.
+   *
+   * @param Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
+      $request = $event->getRequest();
+      $response = $event->getResponse();
+
+      $cacheable = $this->requestPolicy->apply($request) && $this->responsePolicy->apply($response, $request);
+      if ($cacheable && $this->maxAge > 0 && !$this->isCacheControlCustomized($response)) {
+        $this->setCacheable($response, $request);
+      }
+
+      // Response is not cacheable, if Cache-Control remained unset until now or
+      // if the page cache was disabled for this response.
+      if (!$cacheable || !$this->isCacheControlCustomized($response)) {
+        $this->setNotCacheable($response, $request);
+      }
+    }
+  }
+
+  /**
+   * Registers the methods in this class that should be listeners.
+   *
+   * @return array
+   *   An array of event listener definitions.
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::RESPONSE][] = array('onResponse', -50);
+    return $events;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/DefaultCacheControlResponseSubscriber.php b/core/lib/Drupal/Core/PageCache/DefaultCacheControlResponseSubscriber.php
new file mode 100644
index 0000000..cebb204
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DefaultCacheControlResponseSubscriber.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\Core\PageCache\CacheControlResponseSubscriber.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Response subscriber to handle finished responses.
+ */
+class DefaultCacheControlResponseSubscriber extends CacheControlResponseSubscriberBase {
+
+  /**
+   * Whether or not a "Vary: Cookie" header should be added to the response.
+   *
+   * @var bool
+   */
+  protected $omitVaryCookie;
+
+  /**
+   * Construct Cache-Control header generator publicly cacheable responses.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $request_policy
+   *   A policy rule determining the cacheability of a request.
+   * @param \Drupal\Core\PageCache\ResponsePolicyRuleInterface $response_policy
+   *   A policy rule determining the cacheability of a response.
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The configuration factory object.
+   * @param \Drupal\Component\Utility\Settings $settings
+   *   Settings object.
+   */
+  public function __construct(RequestPolicyRuleInterface $request_policy, ResponsePolicyRuleInterface $response_policy, ConfigFactory $config_factory, Settings $settings) {
+    parent::__construct($request_policy, $response_policy, $config_factory->get('system.performance')->get('cache.page.max_age'));
+    $this->omitVaryCookie = (bool) $settings->get('omit_vary_cookie');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setCacheable(Response $response, Request $request) {
+    // HTTP/1.0 proxies do not support the Vary header, so prevent any caching
+    // by sending an Expires date in the past. HTTP/1.1 clients ignore the
+    // Expires header if a Cache-Control: max-age directive is specified (see
+    // RFC 2616, section 14.9.3).
+    if (!$response->headers->has('Expires')) {
+      $this->setExpiresNoCache($response);
+    }
+
+    // Check whether Vary was set to a custom value.
+    //
+    // @todo: Let's get rid of that mechanism and instead instruct site
+    // builders and module authors to swap out
+    // page_cache.cache_control_response_subscriber when custom Vary headers
+    // are required.
+    $vary_customized = (bool) $response->getVary();
+
+    // Replace the default restrictive cache-control header with a permissive
+    // one.
+    $response->headers->set('Cache-Control', 'public');
+    $max_age = !$request->cookies->has(session_name()) || $vary_customized ? $this->getMaxAge() : 0;
+    $response->setMaxAge($max_age);
+
+    // In order to support HTTP cache-revalidation, ensure that there is a
+    // Last-Modified and an ETag header on the response.
+    $created = REQUEST_TIME;
+    if (!$response->headers->has('Last-Modified')) {
+      $response->setLastModified(new \DateTime(gmdate(DATE_RFC1123, REQUEST_TIME)));
+    }
+    else {
+      $created = $response->getLastModified()->getTimestamp();
+    }
+    $response->setEtag($created);
+
+    // Allow HTTP proxies to cache pages for anonymous users without a session
+    // cookie. The Vary header is used to indicates the set of request-header
+    // fields that fully determines whether a cache is permitted to use the
+    // response to reply to a subsequent request for a given URL without
+    // revalidation.
+    if (!$vary_customized && !$this->omitVaryCookie) {
+      $response->setVary('Cookie', FALSE);
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/FastPageCache.php b/core/lib/Drupal/Core/PageCache/FastPageCache.php
new file mode 100644
index 0000000..4644360
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/FastPageCache.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\FastPageCache.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Core\DrupalKernelInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * A page cache implementation wrapping the drupal kernel.
+ */
+class FastPageCache extends PageCacheBase {
+
+  /**
+   * The cache storage backend to use throughout the request/response cycle.
+   *
+   * @var \Drupal\Core\PageCache\StorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Construct a new page cache kernel wrapper.
+   *
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The drual kernel instance to wrap.
+   * @param \Drupal\Core\PageCache\StorageInterface $storage
+   *   The cache storage backend to use throughout the request/response cycle.
+   */
+  public function __construct(DrupalKernelInterface $kernel, StorageInterface $storage) {
+    parent::__construct($kernel);
+    $this->storage = $storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = TRUE) {
+    $response = $this->get($this->storage, $request);
+
+    if (!$response) {
+      $this->boot($request);
+      $response = $this->fetch($request, $type, $catch);
+      $this->set($this->storage, $response, $request);
+    }
+
+    return $this->deliver($response, $request);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCache.php b/core/lib/Drupal/Core/PageCache/PageCache.php
new file mode 100644
index 0000000..1166c07
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCache.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCache.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * A page cache implementation wrapping the drupal kernel.
+ */
+class PageCache extends PageCacheBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = TRUE) {
+    $this->boot($request);
+    $storage = $this->kernel->getContainer()->get('page_cache_storage');
+
+    $response = $this->get($storage, $request);
+    if (!$response) {
+      $response = $this->fetch($request, $type, $catch);
+      $this->set($storage, $response, $request);
+    }
+
+    return $this->deliver($response, $request);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRequestPolicy() {
+    $container = $this->kernel->getContainer();
+    return $container->get('page_cache_request_policy');
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheBase.php b/core/lib/Drupal/Core/PageCache/PageCacheBase.php
new file mode 100644
index 0000000..23a8be5
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheBase.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheKernel.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\DrupalKernelInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\HttpKernel\TerminableInterface;
+
+/**
+ * A base class for page cache implementations wrapping the drupal kernel.
+ */
+abstract class PageCacheBase implements HttpKernelInterface, TerminableInterface {
+
+  /**
+   * The wrapped kernel.
+   *
+   * @var \Drupal\Core\DrupalKernelInterface.
+   */
+  protected $kernel;
+
+  /**
+   * Whether or not the page cache is enabled.
+   *
+   * @var bool
+   */
+  protected $status;
+
+  /**
+   * Construct a new page cache kernel wrapper.
+   *
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The drual kernel instance to wrap.
+   */
+  public function __construct(DrupalKernelInterface $kernel) {
+    $this->kernel = $kernel;
+    $this->status = TRUE;
+  }
+
+  /**
+   * Boot the wrapped kernel and prepare the service container.
+   */
+  protected function boot(Request $request) {
+    $this->kernel->boot();
+    $container = $this->kernel->getContainer();
+
+    // @todo Remove this once everything in the bootstrap has been
+    //   converted to services in the DIC.
+    $container->set('request', $request);
+
+    // Determine whether the internal page cache is enabled by configuration.
+    if (!$container->get('config.factory')->get('system.performance')->get('cache.page.use_internal')) {
+      $this->status = FALSE;
+    }
+  }
+
+  /**
+   * Apply the request policy and attempt to retrieve a response.
+   */
+  protected function get(StorageInterface $storage, Request $request) {
+    if (!$this->getRequestPolicy()->apply($request)) {
+      $this->status = FALSE;
+    }
+    if ($this->status) {
+      return $storage->get($request);
+    }
+  }
+
+  /**
+   * Apply the response policy and attempt to store a response.
+   */
+  protected function set(StorageInterface $storage, Response $response, Request $request) {
+    // Save the rendered markup to the internal page cache.
+    if (!$this->getResponsePolicy()->apply($response, $request)) {
+      $this->status = FALSE;
+    }
+    if ($this->status) {
+      $storage->set($response, $request);
+    }
+  }
+
+  /**
+   * Retrieve response form the backend.
+   */
+  protected function fetch(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = TRUE) {
+    $this->drupalBootstrapCode();
+
+    $response = $this->kernel->handle($request, $type, $catch);
+
+    return $response;
+  }
+
+  /**
+   * Prepare before it is sent to the client.
+   */
+  protected function deliver(Response $response, Request $request) {
+    $response->prepare($request);
+
+    $this->revalidate($response, $request);
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function terminate(Request $request, Response $response) {
+    // The RequestCloseSubscriber currently breaks on lean requests because it
+    // unconditionally calls system_run_automated_cron. Therefore make sure that
+    // the termination-event is only emitted when the response was actually
+    // handled by the kernel (i.e. the full bootstrap phase was reached).
+    //
+    // @see: https://drupal.org/node/2220687
+    if ($this->drupalBootstrapGetPhase() == DRUPAL_BOOTSTRAP_FULL) {
+      $this->kernel->terminate($request, $response);
+    }
+  }
+
+  /**
+   * Return the page cache request policy.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicyRuleInterface
+   *   The default page request cache policy for the given request.
+   */
+  protected function getRequestPolicy() {
+    return RequestPolicy::createDefault();
+  }
+
+  /**
+   * Return the page cache response policy.
+   *
+   * @return \Drupal\Core\PageCache\ResponsePolicyRuleInterface
+   *   The default page cache response policy for the given request.
+   */
+  protected function getResponsePolicy() {
+    $container = $this->kernel->getContainer();
+    return $container->get('page_cache_response_policy');
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo: Use \Symfony\Component\HttpFoundation\Response::isNotModified()
+   */
+  protected function revalidate(Response $response, Request $request) {
+    $last_modified = $response->getLastModified();
+    if (!$last_modified) {
+      return;
+    }
+    $created = $last_modified->getTimestamp();
+
+    // See if the client has provided the required HTTP headers.
+    $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
+    $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
+
+    if ($if_modified_since && $if_none_match
+      && $if_none_match == $response->getEtag() // etag must match
+      && $if_modified_since == $created) {  // if-modified-since must match
+      $response->setStatusCode(304);
+      $response->setContent(null);
+
+      // In the case of a 304 response, certain headers must be sent, and the
+      // remaining may not (see RFC 2616, section 10.3.5).
+      foreach (array_keys($response->headers->all()) as $name) {
+        if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) {
+          $response->headers->remove($name);
+        }
+      }
+    }
+  }
+
+  /**
+   * Load legacy APIs.
+   */
+  protected function drupalBootstrapCode() {
+    drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
+  }
+
+  /**
+   * Return bootstrap phase.
+   */
+  protected function drupalBootstrapGetPhase() {
+    return drupal_get_bootstrap_phase();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheFactory.php b/core/lib/Drupal/Core/PageCache/PageCacheFactory.php
new file mode 100644
index 0000000..bda9264
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheFactory.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheKernelFactory.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\DrupalKernelInterface;
+
+/**
+ * A kernel factory delegating construction to a class defined in settings.
+ */
+class PageCacheFactory implements PageCacheFactoryInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromSettings(DrupalKernelInterface $kernel, Settings $settings) {
+    // Try constructing the page cache instance using a class specified in
+    // page_cache setting.
+    $page_cache_class = $settings->get('page_cache');
+    if (class_exists($page_cache_class)) {
+      $factory_method = array($page_cache_class, 'createFromSettings');
+      if (is_callable($factory_method)) {
+        return call_user_func($factory_method, $kernel, $settings);
+      }
+      else {
+        return new $page_cache_class($kernel);
+      }
+    }
+
+    // Try constructing a fast page cache instance using a storage class
+    // specified in page_cache_storage setting.
+    $storage_class = $settings->get('page_cache_storage');
+    if (class_exists($storage_class)) {
+      $factory_method = array($storage_class, 'createFromSettings');
+      if (is_callable($factory_method)) {
+        $storage = call_user_func($factory_method, $settings);
+      }
+      else {
+        $storage = new $storage_class();
+      }
+      return new FastPageCache($kernel, $storage);
+    }
+
+    // Fall back to the standard container based page cache.
+    return new PageCache($kernel);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php b/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php
new file mode 100644
index 0000000..087f3b0
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheFactoryInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\DrupalKernelInterface;
+
+/**
+ * An interface for page cache kernel factory implementations.
+ */
+interface PageCacheFactoryInterface {
+
+  /**
+   * Construct an appropriate page cache implementation for the given settings.
+   *
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The drual kernel instance to wrap.
+   * @param \Drupal\Component\Utility\Settings $settings
+   *   The settings instance.
+   */
+  public static function createFromSettings(DrupalKernelInterface $kernel, Settings $settings);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicy.php b/core/lib/Drupal/Core/PageCache/RequestPolicy.php
new file mode 100644
index 0000000..71318a0
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicy.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule implementing an allow-deny ACL-model.
+ *
+ * @todo: detailed docs
+ */
+class RequestPolicy implements RequestPolicyRuleInterface {
+
+  /**
+   * A set of policy rules representing the deny-chain.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface[]
+   */
+  protected $deny;
+
+  /**
+   * A set of policy rules representing the allow-chain.
+   *
+   * @var \Drupal\Core\PageCache\RequestPolicyRuleInterface[]
+   */
+  protected $allow;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    foreach ($this->deny as $rule) {
+      if ($rule->apply($request)) {
+        return FALSE;
+      }
+    }
+
+    foreach ($this->allow as $rule) {
+      if ($rule->apply($request)) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Add one policy rule to the deny-chain.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicy
+   *   This policy instance.
+   */
+  public function deny(RequestPolicyRuleInterface $rule) {
+    $this->deny[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+  /**
+   * Add one policy rule to the allow-chain.
+   *
+   * @param \Drupal\Core\PageCache\RequestPolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicy
+   *   This policy instance.
+   */
+  public function allow(RequestPolicyRuleInterface $rule) {
+    $this->allow[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+  /**
+   * Construct and return the default policy.
+   *
+   * @return \Drupal\Core\PageCache\RequestPolicy
+   *   The singleton policy instance.
+   */
+  public static function createDefault() {
+    $policy = new static();
+    $policy->deny(new RequestPolicyRule\CommandLineOrUnsafeMethod());
+    $policy->allow(new RequestPolicyRule\NoSessionOpen());
+
+    return $policy;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php
new file mode 100644
index 0000000..8d86a97
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/CommandLineOrUnsafeMethod.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRule\CommandLineOrUnsafeMethod.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicyRule;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Reject when running from the command line or when HTTP method is not safe.
+ *
+ * An instance of this class is normally used as the global policy by the main
+ * page caching service. The policy allows caching as long as the request was
+ * not initiated from the command line interface (drush) and the request method
+ * is either GET or HEAD (see RFC 2616, section 9.1.1 - Safe Methods).
+ */
+class CommandLineOrUnsafeMethod implements RequestPolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return $this->isCliRequest($request) || !$request->isMethodSafe();
+  }
+
+  /**
+   * Exclude a page request when run from a command line script.
+   */
+  protected function isCliRequest(Request $request) {
+    return drupal_is_cli();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php
new file mode 100644
index 0000000..e048d8a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRule/NoSessionOpen.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRule\NoSessionOpen.
+ */
+
+namespace Drupal\Core\PageCache\RequestPolicyRule;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule evaluating to TRUE only when there is no session open.
+ *
+ * Do not serve cached pages to authenticated users, or to anonymous users when
+ * $_SESSION is non-empty. $_SESSION may contain status messages from a form
+ * submission, the contents of a shopping cart, or other userspecific content
+ * that should not be cached and displayed to other users.
+ */
+class NoSessionOpen implements RequestPolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return !$request->cookies->has(session_name());
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php b/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php
new file mode 100644
index 0000000..4336dc8
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/RequestPolicyRuleInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\RequestPolicyRuleInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for request policy rule implementations.
+ */
+interface RequestPolicyRuleInterface {
+
+  /**
+   * Apply the policy rule to the given request and return the result.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return bool
+   *   TRUE when the policy rule allows caching for the request.
+   */
+  public function apply(Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicy.php b/core/lib/Drupal/Core/PageCache/ResponsePolicy.php
new file mode 100644
index 0000000..2c78abe
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicy.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Container for rules applied before a response is recorded in the cache.
+ */
+class ResponsePolicy implements ResponsePolicyRuleInterface {
+
+  /**
+   * A set of policy rules representing the deny-chain.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicyRuleInterface[]
+   */
+  protected $deny;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    foreach ($this->deny as $rule) {
+      if ($rule->apply($response, $request)) {
+        return FALSE;
+      }
+    }
+
+    return TRUE;
+  }
+
+  /**
+   * Add one policy rule to the deny-chain.
+   *
+   * @param \Drupal\Core\PageCache\ResponsePolicyRuleInterface $rule
+   *   The rules to add to the deny-chain.
+   *
+   * @return \Drupal\Core\PageCache\ResponsePolicy
+   *   This policy instance.
+   */
+  public function deny(ResponsePolicyRuleInterface $rule) {
+    $this->deny[spl_object_hash($rule)] = $rule;
+    return $this;
+  }
+
+  /**
+   * Construct and return the default response policy.
+   *
+   * @return \Drupal\Core\PageCache\ResponsePolicy
+   *   The default policy instance.
+   */
+  public static function createDefault() {
+    $policy = new static();
+    $policy->deny(new ResponsePolicyRule\IsBinaryFileResponse());
+
+    return $policy;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php
new file mode 100644
index 0000000..5087c1a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/IsBinaryFileResponse.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRule\IsBinaryFileResponse.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicyRule;
+
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
+use Symfony\Component\HttpFoundation\BinaryFileResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A veto rule evaluating to TRUE when the kill switch was triggered.
+ */
+class IsBinaryFileResponse implements ResponsePolicyRuleInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    return ($response instanceof BinaryFileResponse);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php
new file mode 100644
index 0000000..53b1384
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRule/KillSwitch.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRule\KillSwitch.
+ */
+
+namespace Drupal\Core\PageCache\ResponsePolicyRule;
+
+use Drupal\Core\PageCache\ResponsePolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * A veto rule evaluating to TRUE when the kill switch was triggered.
+ */
+class KillSwitch implements ResponsePolicyRuleInterface {
+
+  /**
+   * A flag indicating whether the kill switch was triggered.
+   *
+   * @var bool
+   */
+  protected $kill = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Response $response, Request $request) {
+    return $this->kill;
+  }
+
+  /**
+   * Deny any page caching on the current request.
+   */
+  public function trigger() {
+    $this->kill = TRUE;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php b/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php
new file mode 100644
index 0000000..86ec452
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/ResponsePolicyRuleInterface.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ResponsePolicyRuleInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines the interface for response policy rule implementations.
+ */
+interface ResponsePolicyRuleInterface {
+
+  /**
+   * Apply the policy rule to the given response and return the result.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return bool
+   *   TRUE when the policy rule allows caching of the response.
+   */
+  public function apply(Response $response, Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php b/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php
new file mode 100644
index 0000000..02ecc32
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CidGenerator.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Core\ContentNegotiation;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default page cache-id generator.
+ */
+class CidGenerator implements CidGeneratorInterface {
+
+  /**
+   * Content negotiation service.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Constructs a new page cache-id generator.
+   *
+   * @param \Drupal\Core\ContentNegotiation $negotiation
+   *   Content negotiation service.
+   */
+  public function __construct(ContentNegotiation $negotiation) {
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCid(Request $request) {
+    $cid_parts = array(
+      $request->getUri(),
+      $this->negotiation->getContentType($request),
+    );
+    return sha1(implode(':', $cid_parts));
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php b/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php
new file mode 100644
index 0000000..c4b31d1
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CidGeneratorInterface.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines an interface for page cache-id generators.
+ */
+interface CidGeneratorInterface {
+
+  /**
+   * Generate the page cache cid for a request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return string
+   *   The cid for a request.
+   */
+  public function getCid(Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php b/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php
new file mode 100644
index 0000000..c4c1c8a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CompressionHelper.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Decompress a response for clients not accepting gzip encoded content.
+ */
+class CompressionHelper {
+
+  /**
+   * Compress the response body and add appropriate headers.
+   */
+  public function compress(Response $response, Request $request) {
+    if (!$response->headers->get('Content-Encoding')) {
+      $content = $response->getContent();
+      if ($content) {
+        $response->setContent(gzencode($content, 9, FORCE_GZIP));
+        $response->headers->set('Content-Encoding', 'gzip');
+      }
+
+      // When page compression is enabled, ensure that proxy caches will record
+      // and deliver different versions of a page depending on whether the
+      // client supports gzip or not.
+      $response->setVary('Accept-Encoding', FALSE);
+    }
+  }
+
+  /**
+   * Uncompress a response body if the client cannot handle gzipped data.
+   */
+  public function uncompress(Response $response, Request $request) {
+    if ($response->headers->get('Content-Encoding') == 'gzip') {
+      if (strpos($request->headers->get('Accept-Encoding'), 'gzip') === FALSE) {
+        // The client does not support compression. Decompress the content and
+        // remove the Content-Encoding header.
+        $content = $response->getContent();
+        $content = gzinflate(substr(substr($content, 10), 0, -8));
+        $response->setContent($content);
+        $response->headers->remove('Content-Encoding');
+      }
+      else {
+        // The response content is already gzip'ed, so make sure
+        // zlib.output_compression does not compress it once more.
+        ini_set('zlib.output_compression', '0');
+      }
+    }
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/DatabaseFactory.php b/core/lib/Drupal/Core/PageCache/Storage/DatabaseFactory.php
new file mode 100644
index 0000000..bb81edb
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/DatabaseFactory.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\DatabaseFactory.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Cache\DatabaseBackend;
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Database\Database;
+
+/**
+ * A page cache storage factory capable of operating before the kernel exists.
+ */
+class DatabaseFactory {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createFromSettings(Settings $settings) {
+    $bin = new DatabaseBackend(Database::getConnection(), 'page');
+    $content_negotiation = new ContentNegotiation();
+    $cid_generator = new CidGenerator($content_negotiation);
+    $use_compression = $settings->get('page_cache_response_gzip', FALSE);
+
+    return new InternalCache($bin, $cid_generator, $use_compression);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php b/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php
new file mode 100644
index 0000000..59d684b
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\InternalCache.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\EventSubscriber\HtmlViewSubscriber;
+use Drupal\Core\PageCache\StorageInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache storage backend based on the internal cache.
+ */
+class InternalCache implements StorageInterface {
+
+  /**
+   * Cache backend for the page cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Generator for the cache id.
+   *
+   * @var \Drupal\Core\PageCache\CidGeneratorInterface
+   */
+  protected $cidGenerator;
+
+  /**
+   * A flag indicating whether compression of cached objects should be enabled.
+   *
+   * @var bool
+   */
+  protected $useCompression;
+
+  /**
+   * Construct a new cache backend with internal cache storage.
+   *
+   * If compression is enabled, a gzipped version of the page is stored in the
+   * cache to avoid compressing the output on each request. The cache entry is
+   * unzipped in the relatively rare event that the page is requested by a
+   * client without gzip support.
+   *
+   * Page compression requires the PHP zlib extension
+   * (http://php.net/manual/ref.zlib.php).
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend to use.
+   * @param \Drupal\Core\PageCache\Storage\CidGeneratorInterface $cid_generator
+   *   Generator for the cache id.
+   * @param bool $use_compression
+   *   A flag indicating whether compression of cached objects should be
+   *   enabled.
+   */
+  public function __construct(CacheBackendInterface $cache, CidGeneratorInterface $cid_generator, $use_compression) {
+    $this->cache = $cache;
+    $this->cidGenerator = $cid_generator;
+    $this->useCompression = $use_compression && extension_loaded('zlib');
+  }
+
+  /**
+   * A static factory method used to instantiate the class as a service.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend to use.
+   * @param \Drupal\Core\PageCache\Storage\CidGeneratorInterface $cid_generator
+   *   Generator for the cache id.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory instance.
+   *
+   * @return \Drupal\Core\PageCache\Storage\InternalCache
+   *   A fully configured page cache storage instance.
+   */
+  public static function createFromConfig(CacheBackendInterface $cache, CidGeneratorInterface $cid_generator, ConfigFactoryInterface $config_factory) {
+    $use_compression = $config_factory->get('system.performance')->get('response.gzip');
+    return new static($cache, $cid_generator, $use_compression);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get(Request $request) {
+    $cid = $this->cidGenerator->getCid($request);
+    $cache = $this->cache->get($cid);
+    if (!is_object($cache)) {
+      return;
+    }
+
+    // Retrieve the response from the cache object.
+    $response = $cache->data;
+
+    // If there is no Last-Modified header on the cached response, compute it
+    // from the creation date of the cache-object.
+    if (!$response->headers->has('Last-Modified')) {
+      $date = new \DateTime();
+      $date->setTimestamp($cache->created);
+      $response->setLastModified($date);
+    }
+
+    // Uncompress the response when serving clients which cannot handle gzip.
+    // In order to prevent garbled pages when server configuration changes,
+    // decompressing the response is done unconditionally.
+    $this->getCompressionHelper()->uncompress($response, $request);
+
+    $response->headers->set('X-Drupal-Cache', 'HIT');
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set(Response $response, Request $request) {
+    // Use the actual timestamp from an Expires header, if available.
+    $date = $response->getExpires();
+    $expire = $date && $date->getTimestamp() > REQUEST_TIME ? $date->getTimestamp() : Cache::PERMANENT;
+
+    if ($this->useCompression) {
+      $compression_helper = $this->getCompressionHelper();
+      $compression_helper->compress($response, $request);
+    }
+
+    $cid = $this->cidGenerator->getCid($request);
+    $tags = HtmlViewSubscriber::convertHeaderToCacheTags($response->headers->get('X-Drupal-Cache-Tags'));
+    $this->cache->set($cid, $response, $expire, $tags);
+
+    // Uncompress the response when serving clients which cannot handle gzip.
+    if (isset($compression_helper)) {
+      $compression_helper->uncompress($response, $request);
+    }
+
+    $response->headers->set('X-Drupal-Cache', 'MISS');
+  }
+
+  /**
+   * Construct and return the compression helper.
+   *
+   * @return \Drupal\Core\PageCache\Storage\CompressionHelper
+   *   A helper object implementing compression/uncompression of responses.
+   */
+  protected function getCompressionHelper() {
+    return new CompressionHelper();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/PageCache/StorageInterface.php b/core/lib/Drupal/Core/PageCache/StorageInterface.php
new file mode 100644
index 0000000..83bf98b
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/StorageInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\StorageInterface
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines an interface for internal page cache storage.
+ */
+interface StorageInterface {
+
+  /**
+   * Retrieve a response object from the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The incoming request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|NULL
+   *   A response object complete with headers and content.
+   */
+  public function get(Request $request);
+
+  /**
+   * Save a response object to the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object complete with headers and content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object the response was generated for.
+   */
+  public function set(Response $response, Request $request);
+
+}
diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php
index 85864e1..c141709 100644
--- a/core/modules/statistics/statistics.php
+++ b/core/modules/statistics/statistics.php
@@ -11,6 +11,7 @@
 // Load the Drupal bootstrap.
 require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php';
 require_once dirname(dirname(__DIR__)) . '/includes/bootstrap.inc';
+require_once dirname(dirname(__DIR__)) . '/includes/database.inc';
 drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 
 if (\Drupal::config('statistics.settings')->get('count_content_views')) {
diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
index 029bcb8..a2440bb 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
@@ -189,9 +189,13 @@ function testPageCache() {
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'must-revalidate, no-cache, post-check=0, pre-check=0, private', 'Cache-Control header was sent.');
     $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
     $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.');
+  }
 
+  /**
+   * Tests cache headers.
+   */
+  public function testOmitVaryCookie() {
     // Check the omit_vary_cookie setting.
-    $this->drupalLogout();
     $settings['settings']['omit_vary_cookie'] = (object) array(
       'value' => TRUE,
       'required' => TRUE,
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
index d5fc878..baca823 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
@@ -23,10 +23,22 @@ class ToolbarController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function subtreesJsonp() {
-    _toolbar_initialize_page_cache();
     $subtrees = toolbar_get_rendered_subtrees();
     $response = new JsonResponse($subtrees);
     $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
+
+    // The Expires HTTP header is the heart of the client-side HTTP caching. The
+    // additional server-side page cache only takes effect when the client
+    // accesses the callback URL again (e.g., after clearing the browser cache
+    // or when force-reloading a Drupal page).
+    $max_age = 365 * 24 * 60;
+    $response->setPrivate();
+    $response->setMaxAge($max_age);
+
+    $expires = new \DateTime();
+    $expires->setTimestamp(REQUEST_TIME + $max_age);
+    $response->setExpires($expires);
+
     return $response;
   }
 
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php
new file mode 100644
index 0000000..ae4e408
--- /dev/null
+++ b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/IsToolbarPath.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\toolbar\PageCache\IsToolbarPath.
+ */
+
+namespace Drupal\toolbar\PageCache;
+
+use Drupal\Core\PageCache\RequestPolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for the toolbar page cache service.
+ *
+ * This policy allows caching of requests directed to /toolbar/subtrees/{hash}
+ * even for authenticated users.
+ */
+class IsToolbarPath implements RequestPolicyRuleInterface {
+
+  /**
+   * Test whether this request goes to /toolbar/subtrees.
+   */
+  public function apply(Request $request) {
+    return strpos($request->getPathInfo(), '/toolbar/subtrees/') === 0;
+  }
+
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 6e3e4e3..064336c 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -105,48 +105,6 @@ function toolbar_element_info() {
 }
 
 /**
- * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
- *
- * This gets invoked after full bootstrap, so must duplicate some of what's
- * done by _drupal_bootstrap_page_cache().
- *
- * @todo Replace this hack with something better integrated with DrupalKernel
- *   once Drupal's page caching itself is properly integrated.
- */
-function _toolbar_initialize_page_cache() {
-  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
-  drupal_page_is_cacheable(TRUE);
-
-  // If we have a cache, serve it.
-  // @see _drupal_bootstrap_page_cache()
-  $request = \Drupal::request();
-  $cache = drupal_page_get_cache($request);
-  if (is_object($cache)) {
-    $response = new Response();
-    $response->headers->set('X-Drupal-Cache', 'HIT');
-    date_default_timezone_set(drupal_get_user_timezone());
-
-    drupal_serve_page_from_cache($cache, $response, $request);
-
-    $response->prepare($request);
-    $response->send();
-    // We are done.
-    exit;
-  }
-
-  // Otherwise, create a new page response (that will be cached).
-  drupal_add_http_header('X-Drupal-Cache', 'MISS');
-
-  // The Expires HTTP header is the heart of the client-side HTTP caching. The
-  // additional server-side page cache only takes effect when the client
-  // accesses the callback URL again (e.g., after clearing the browser cache or
-  // when force-reloading a Drupal page).
-  $max_age = 3600 * 24 * 365;
-  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
-}
-
-/**
  * Implements hook_page_build().
  *
  * Add admin toolbar to the page_top region automatically.
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f26968..eacdb01 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -6,3 +6,7 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [toolbar]
+  toolbar.page_cache_policy.toolbar_path:
+    class: Drupal\toolbar\PageCache\IsToolbarPath
+    tags:
+      - { name: page_cache_request_policy.allow }
diff --git a/core/update.php b/core/update.php
index 45904a2..a2e7e19 100644
--- a/core/update.php
+++ b/core/update.php
@@ -345,7 +345,7 @@ function update_task_list($active = NULL) {
 $container->get('request_stack')->push($request);
 
 // Determine if the current user has access to run update.php.
-drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 
 require_once DRUPAL_ROOT . '/' . Settings::get('session_inc', 'core/includes/session.inc');
 drupal_session_initialize();
