diff --git a/core/core.services.yml b/core/core.services.yml
index 021ac1a..4dc5a75 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1090,11 +1090,28 @@ services:
   session_configuration:
     class: Drupal\Core\Session\SessionConfiguration
     arguments: ['%session.storage.options%']
+  session_handler:
+    alias: session_handler.storage
+  session_handler.storage:
+    class: Drupal\Core\Session\SessionHandler
+    arguments: ['@request_stack', '@database']
+    tags:
+      - { name: backend_overridable }
+  session_handler.write_check:
+    class: Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler
+    tags:
+      - { name: session_handler_proxy, priority: 100 }
+  session_handler.write_safe:
+    class: Drupal\Core\Session\WriteSafeSessionHandler
+    tags:
+      - { name: session_handler_proxy, priority: 150 }
   session_manager:
     class: Drupal\Core\Session\SessionManager
     arguments: ['@request_stack', '@database', '@session_manager.metadata_bag', '@session_configuration']
     tags:
       - { name: backend_overridable }
+    calls:
+      - [setSaveHandler, ['@session_handler']]
   session_manager.metadata_bag:
     class: Drupal\Core\Session\MetadataBag
     arguments: ['@settings']
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 82fa4c4..6baf4df 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -13,6 +13,7 @@
 use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteEnhancers;
 use Drupal\Core\DependencyInjection\Compiler\RegisterLazyRouteFilters;
 use Drupal\Core\DependencyInjection\Compiler\StackedKernelPass;
+use Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass;
 use Drupal\Core\DependencyInjection\Compiler\RegisterStreamWrappersPass;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -62,6 +63,8 @@ public function register(ContainerBuilder $container) {
 
     $container->addCompilerPass(new StackedKernelPass());
 
+    $container->addCompilerPass(new StackedSessionHandlerPass());
+
     $container->addCompilerPass(new MainContentRenderersPass());
 
     // Collect tagged handler services as method calls on consumer services.
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/StackedSessionHandlerPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/StackedSessionHandlerPass.php
new file mode 100644
index 0000000..32ef407
--- /dev/null
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/StackedSessionHandlerPass.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\DependencyInjection\Compiler\StackedSessionHandlerPass.
+ */
+
+namespace Drupal\Core\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Provides a compiler pass for stacked session save handlers.
+ */
+class StackedSessionHandlerPass implements CompilerPassInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(ContainerBuilder $container) {
+
+    if ($container->hasDefinition('session_handler')) {
+      return;
+    }
+
+    $session_handler_proxies = [];
+    $priorities = [];
+
+    foreach ($container->findTaggedServiceIds('session_handler_proxy') as $id => $attributes) {
+      $priorities[$id] = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+      $session_handler_proxies[$id] = $container->getDefinition($id);
+    }
+
+    array_multisort($priorities, SORT_ASC, $session_handler_proxies);
+
+    $decorated_id = 'session_handler.storage';
+    foreach ($session_handler_proxies as $id => $decorator) {
+      // Prepend the inner session handler as first constructor argument.
+      $arguments = $decorator->getArguments();
+      array_unshift($arguments, new Reference($decorated_id));
+      $decorator->setArguments($arguments);
+
+      $decorated_id = $id;
+    }
+
+    $container->setAlias('session_handler', $decorated_id);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Session/SessionManager.php b/core/lib/Drupal/Core/Session/SessionManager.php
index c3e24f5..00998cb 100644
--- a/core/lib/Drupal/Core/Session/SessionManager.php
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -10,7 +10,6 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Core\Database\Connection;
 use Symfony\Component\HttpFoundation\RequestStack;
-use Symfony\Component\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler;
 use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
 
 /**
@@ -90,13 +89,7 @@ public function __construct(RequestStack $request_stack, Connection $connection,
     $this->requestStack = $request_stack;
     $this->connection = $connection;
 
-    // Register the default session handler.
-    // @todo Extract session storage from session handler into a service.
-    $save_handler = new SessionHandler($this->requestStack, $this->connection);
-    $write_check_handler = new WriteCheckSessionHandler($save_handler);
-    $this->writeSafeHandler = new WriteSafeSessionHandler($write_check_handler);
-
-    parent::__construct($options, $this->writeSafeHandler, $metadata_bag);
+    parent::__construct($options, NULL, $metadata_bag);
 
     // @todo When not using the Symfony Session object, the list of bags in the
     //   NativeSessionStorage will remain uninitialized. This will lead to
@@ -293,6 +286,19 @@ public function enable() {
   }
 
   /**
+   * {@inheritdoc}
+   */
+  public function setSaveHandler($saveHandler = null) {
+    if ($saveHandler instanceof WriteSafeSessionHandlerInterface) {
+      $this->writeSafeHandler = $saveHandler;
+    }
+    else {
+      $this->writeSafeHandler = \Drupal::service('session_handler.write_safe');
+    }
+    parent::setSaveHandler($saveHandler);
+  }
+
+  /**
    * Returns whether the current PHP process runs on CLI.
    *
    * Command line clients do not support cookies nor sessions.
diff --git a/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php b/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php
new file mode 100644
index 0000000..65b55a6
--- /dev/null
+++ b/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Session\StackSessionHandlerIntegrationTest.
+ */
+
+namespace Drupal\system\Tests\Session;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests the stacked session handler functionality.
+ *
+ * @group Session
+ */
+class StackSessionHandlerIntegrationTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('session_test');
+
+  /**
+   * Tests a request.
+   */
+  public function testRequest() {
+    $actual_trace = $this->drupalGetAjax('session-test/trace-handler');
+    $expect_trace = [
+      ['BEGIN', 'test_argument', 'open'],
+      ['BEGIN', NULL, 'open'],
+      ['END', NULL, 'open'],
+      ['END', 'test_argument', 'open'],
+      ['BEGIN', 'test_argument', 'read', $this->sessionId],
+      ['BEGIN', NULL, 'read', $this->sessionId],
+      ['END', NULL, 'read', $this->sessionId],
+      ['END', 'test_argument', 'read', $this->sessionId],
+      ['BEGIN', 'test_argument', 'write', $this->sessionId],
+      ['BEGIN', NULL, 'write', $this->sessionId],
+      ['END', NULL, 'write', $this->sessionId],
+      ['END', 'test_argument', 'write', $this->sessionId],
+      ['BEGIN', 'test_argument', 'close'],
+      ['BEGIN', NULL, 'close'],
+      ['END', NULL, 'close'],
+      ['END', 'test_argument', 'close'],
+    ];
+    $this->assertEqual($expect_trace, $actual_trace);
+  }
+
+}
diff --git a/core/modules/system/tests/modules/session_test/session_test.routing.yml b/core/modules/system/tests/modules/session_test/session_test.routing.yml
index e28d40d..8b9393e 100644
--- a/core/modules/system/tests/modules/session_test/session_test.routing.yml
+++ b/core/modules/system/tests/modules/session_test/session_test.routing.yml
@@ -75,3 +75,11 @@ session_test.form:
     _title: 'Test form'
   requirements:
     _access: 'TRUE'
+
+session_test.trace_handler:
+  path: '/session-test/trace-handler'
+  defaults:
+    _title: 'Returns the trace recorded by test proxy session handlers as JSON'
+    _controller: '\Drupal\session_test\Controller\SessionTestController::traceHandler'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/system/tests/modules/session_test/session_test.services.yml b/core/modules/system/tests/modules/session_test/session_test.services.yml
index 281b09d..b38505d 100644
--- a/core/modules/system/tests/modules/session_test/session_test.services.yml
+++ b/core/modules/system/tests/modules/session_test/session_test.services.yml
@@ -4,3 +4,14 @@ services:
     arguments: ['@session_manager']
     tags:
       - { name: event_subscriber }
+  session_test.session_handler.test_proxy:
+    class: Drupal\session_test\Session\TestSessionHandlerProxy
+    tags:
+      - { name: session_handler_proxy }
+  session_test.session_handler.test_proxy2:
+    class: Drupal\session_test\Session\TestSessionHandlerProxy
+    arguments: ['test_argument']
+    tags:
+      - { name: session_handler_proxy, priority: 20 }
+  session_test.session_handler_proxy_trace:
+    class: ArrayObject
diff --git a/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php b/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
index abaaa4c..249b1ea 100644
--- a/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
+++ b/core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Controller\ControllerBase;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -125,4 +126,27 @@ public function isLoggedIn() {
     return ['#markup' => $this->t('User is logged in.')];
   }
 
+  /**
+   * Returns the trace recorded by test proxy session handlers as JSON.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse
+   *   The response.
+   */
+  public function traceHandler() {
+    // Start a session if necessary, set a value and then save and close it.
+    \Drupal::service('session_manager')->start();
+    if (empty($_SESSION['trace-handler'])) {
+      $_SESSION['trace-handler'] = 1;
+    }
+    else {
+      $_SESSION['trace-handler']++;
+    }
+    \Drupal::service('session_manager')->save();
+
+    // Collect traces and return them in JSON format.
+    $trace = \Drupal::service('session_test.session_handler_proxy_trace')->getArrayCopy();
+
+    return new JsonResponse($trace);
+  }
+
 }
diff --git a/core/modules/system/tests/modules/session_test/src/Session/TestSessionHandlerProxy.php b/core/modules/system/tests/modules/session_test/src/Session/TestSessionHandlerProxy.php
new file mode 100644
index 0000000..859ddb9
--- /dev/null
+++ b/core/modules/system/tests/modules/session_test/src/Session/TestSessionHandlerProxy.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\session_test\Session\TestSessionHandlerProxy.
+ */
+
+namespace Drupal\session_test\Session;
+
+/**
+ * Provides a test session handler proxy.
+ */
+class TestSessionHandlerProxy implements \SessionHandlerInterface {
+
+  /**
+   * The decorated session handler.
+   *
+   * @var \SessionHandlerInterface
+   */
+  protected $sessionHandler;
+
+  /**
+   * An optional argument.
+   *
+   * @var mixed
+   */
+  protected $optionalArgument;
+
+  /**
+   * Constructs a new TestSessionHandlerProxy object.
+   *
+   * @param \SessionHandlerInterface $session_handler
+   *   The decorated session handler.
+   * @param mixed $optional_argument
+   *   (optional) An optional argument.
+   */
+  public function __construct(\SessionHandlerInterface $session_handler, $optional_argument = NULL) {
+    $this->sessionHandler = $session_handler;
+    $this->optionalArgument = $optional_argument;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function open($save_path, $name) {
+    $trace = \Drupal::service('session_test.session_handler_proxy_trace');
+    $trace[] = ['BEGIN', $this->optionalArgument, __FUNCTION__];
+    $result = $this->sessionHandler->open($save_path, $name);
+    $trace[] = ['END', $this->optionalArgument, __FUNCTION__];
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function close() {
+    $trace = \Drupal::service('session_test.session_handler_proxy_trace');
+    $trace[] = ['BEGIN', $this->optionalArgument, __FUNCTION__];
+    $result = $this->sessionHandler->close();
+    $trace[] = ['END', $this->optionalArgument, __FUNCTION__];
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function read($session_id) {
+    $trace = \Drupal::service('session_test.session_handler_proxy_trace');
+    $trace[] = ['BEGIN', $this->optionalArgument, __FUNCTION__, $session_id];
+    $result = $this->sessionHandler->read($session_id);
+    $trace[] = ['END', $this->optionalArgument, __FUNCTION__, $session_id];
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function write($session_id, $session_data) {
+    $trace = \Drupal::service('session_test.session_handler_proxy_trace');
+    $trace[] = ['BEGIN', $this->optionalArgument, __FUNCTION__, $session_id];
+    $result = $this->sessionHandler->write($session_id, $session_data);
+    $trace[] = ['END', $this->optionalArgument, __FUNCTION__, $session_id];
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function destroy($session_id) {
+    return $this->sessionHandler->destroy($session_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function gc($max_lifetime) {
+    return $this->sessionHandler->gc($max_lifetime);
+  }
+
+}
