PHP Session Handling on Heroku

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:

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:

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: