PHP Session Handling on Heroku

Last Updated: 15 June 2015

memcache php session

Table of Contents

HTTP is a stateless protocol, but for most applications, it’s necessary to preserve certain information (such as login state or the contents of a shopping cart) across requests.

Sessions are the solution, and the information can either be stored in encrypted HTTP cookies on the client side, or in some sort of storage on the server side (with an HTTP cookie holding only the session ID so the server can identify the client).

Both approaches have their advantages and disadvantages, but for server side sessions, some configuration is necessary. This article shows how to reliably handle sessions in PHP applications on Heroku.

The need for sessions

Whenever an application runs on more than one server (or dyno, as is the case at Heroku), with dynos receiving traffic randomly from a load balancer, PHP’s session support needs to be configured to not use standard file-based sessions.

If sessions are stored on each dyno’s file system, the next request from a user (who in this example is assumed to have previously been logged in, so the request would contain a session cookie) could end up on a different dyno, where the session information does not exist.

A common solution in the past has been the concept of “sticky sessions” where the load balancer makes sure to always send users to the same backend server. This approach is problematic for various reasons as it negatively affects scalability and durability e.g. in the event of a backend server outage.

Luckily, horizontal scalability (i.e. scaling out by using additional servers instead of scaling vertically by using bigger servers) has always been at the heart of PHP, so numerous options for distributed sessions are available natively in PHP and in all of the popular frameworks.

Session storage options

Sessions are typically stored either in suitably distributed or scalable data stores such as Memcache or Redis, or relational databases like PostgreSQL or MySQL.

Popular PHP extensions such as memcached or redis provide native session handlers that are easy to use, and userland libraries like the predis Redis library frequently provide session handlers (and often examples) as well.

In the following example, we’ll be using a Heroku add-on to store sessions in Memcached.

Storing sessions in Memcached

Application setup

If you haven’t created a Git repository and Heroku application yet, do so first:

$ mkdir heroku-session-example
$ cd heroku-session-example
$ git init
$ heroku create

Enabling the memcached extension

First up, you will need to list the memecached PHP extension as a requirement for your application so Heroku knows to activate it upon push.

The easiest way (if you have the extension installed locally for development and testing) is to run composer require ext-memcached:* to declare a dependency on the extension.

Alternatively, you can add the dependency to your composer.json by hand. Either way, your composer.json should then look like the following (assuming you have no other dependencies in your project):

{
    "require": {
        "ext-memcached": "*"
    }
}

If you have any library dependencies in your project, run composer update to make sure the extension requirement ends up in your composer.lock.

Next, add this change to your Git repository:

$ git add composer.*
$ git commit -m "require memcached ext"

Provisioning a Memcache add-on

Next up, add a Memcached add-on from the Heroku add-ons marketplace. This example uses MemCachier; the steps are similar for Memcached Cloud (except of course for the different environment variable names):

$ heroku addons:create memcachier

Configuring PHP to use memcached for sessions

To configure PHP to use the memcached session handler, you can simply define custom PHP settings with a .user.ini file in the root of your application (or, if you’re using a custom document root, into that directory) with the following contents:

session.save_handler=memcached
session.save_path=${MEMCACHIER_SERVERS}
memcached.sess_binary=1
memcached.sess_sasl_username=${MEMCACHIER_USERNAME}
memcached.sess_sasl_password=${MEMCACHIER_PASSWORD}

These settings configure the servers to use, switches on the binary protocol for the connection (so we can use authentication), and sets the SASL authentication details.

This approach is taking advantage of a php.ini feature that allows referencing environment variables. All Heroku config vars are exposed to an application’s environment, and when you provisioned the Memcachier add-on, several config vars, including the three MEMCACHIER_... variables we’re referencing above, were set automatically for you.

If you’re using the Memcached Cloud instead of the MemCachier add-on, use the corresponding MEMCACHEDCLOUD_... environment variable names instead.

As SASL authentication incurs some overhead on connect, it is a good idea to make the session connection persistent, which you can do using a prefix for the session save path:

session.save_path="PERSISTENT=myapp_session ${MEMCACHIER_SERVERS}"

Alternatively, you can use a couple of ini_set() calls in your code instead:

ini_set('session.save_handler=memcached');
ini_set('session.save_path', 'PERSISTENT=myapp_session ' . getenv('MEMCACHIER_SERVERS'));
ini_set('memcached.sess_binary', 1);
ini_set('memcached.sess_sasl_username', getenv('MEMCACHIER_USERNAME'));
ini_set('memcached.sess_sasl_password', getenv('MEMCACHIER_PASSWORD'));

Once you’re ready, add and push your changes:

$ git add .user.ini # or the PHP file you added the ini_set() calls to
$ git commit -m "session settings for memcached"

Testing sessions

In your code, you can now easily test your sessions. For instance, in a quick index.php for testing (assuming you’re using the .user.ini approach above; if not, insert the ini_set() calls`):

<?php

// any ini_set() for session configuration goes here when not using .user.ini

session_start();
if (!isset($_SESSION['count'])) {
    $_SESSION['count'] = 0;
}
$_SESSION['count']++;

echo "Hello #" . $_SESSION['count'];

You are now ready to add, commit and push to Heroku:

$ git add index.php
$ git commit -m "session test";
$ git push heroku master

Run heroku open or manually point your browser to your application to see a greeting like “Hello #1”, and observe how as you refresh the page, the count keeps increasing. When using a different browser or clearing your cache, the count resets back to 1.

The free and hobby dyno types only support a maximum of one dyno running per process type. To scale, you will need use the professional dyno types.

You can now scale up your number of dynos to make sure that even as your requests hit different dynos, the session data is shared across them:

$ heroku ps:scale web=5

Wait a few seconds for the new dynos to boot, then refresh the page a few times and observe how the count does not drop back to 1. If you’re curious, run heroku logs (or heroku logs --tail; use Ctrl-C to exit again) to see how your requests are handled by different dyno instances (web.1 through web.5).

You probably don’t need five web dynos right now, so scale back to 1.

$ heroku ps:scale web=1

Configuring Symfony2 to use the native session handler

To easily use the approach above with Symfony2, instruct the framework to use the native session handler registered with PHP using the following configuration section in app/config/config.yml:

framework:
    session:
        # null ("~") triggers use of default PHP session handler
        handler_id:  ~