Configuring Craft for Load-Balanced Environments

High-volume sites can run the risk of overwhelming a single web server. A common approach to mediating that risk is to spread the web traffic over multiple web servers, called load balancing.

Load balancing with Craft is similar to most other PHP web apps, where you’ll likely need to adjust your setup so everything runs smoothly on multiple web servers.

Load Balanced Diagram

PHP Sessions #

PHP sessions are file-based by default. Using a load balancer means one visitor may communicate with multiple servers and have a unique session for each one, leading to an unstable experience.

One symptom of PHP sessions not being configured correctly is experiencing constant, unexpected logouts from the Craft control panel.

There are several ways of handling this.

1. Save Sessions to a Memory Key-Value Store #

We recommend configuring PHP and Craft to store PHP sessions in an in-memory key-value store, like Redis. This option is the most performant and scalable of the solutions.

2. Save Sessions to the Database #

PHP and Craft can also be configured to store session information in the database.

3. Configure Session Affinity on the Load Balancer #

If you have control over your load balancer, one solution is to configure the load balancer’s session affinity, sometimes called “sticky sessions.” Doing this will ensure a user is always sent back to the same server their PHP session started on.

4. Save Sessions to Shared Storage #

Another solution is to save your sessions to shared, persistent storage. Point php.ini’s session.save_path to a shared folder all the web servers can access, such as an NFS mount.

Modifying sessions.save_path will stop PHP’s garbage collection from running automatically. It’s up to you to clean up session files via some other mechanism like a cron job.

Mutex Locks #

Like PHP sessions, mutex locks are file-based by default, so they won’t be reliable for load-balanced environments.

Yii includes mutex drivers for MySQL and PostgreSQL, however MySQL lock handling can be erratic in many cases so we no longer recommend it. Instead we recommend using an in-memory key-value store like Redis.

To configure a custom mutex driver in Craft 3.7.30 or later, you should leave the main mutex component intact, and configure its nested $mutex property instead. For example, here’s how you can hook it up to the yii\redis\Mutex driver provided by yii2-redis:

return [
    'components' => [
        'mutex' => [
            'mutex' => yii\redis\Mutex::class,
            'redis' => [
                // Redis server connection info
            ],
        ],
    ],
];

Assets #

Like PHP sessions, you’ll want uploaded files like images and PDFs to be available to all the web servers.

Pointing the web servers to a network share, like an NFS mount, is one option.

An easier approach, however, is to configure Craft to use a cloud-based storage service like S3 using the Amazon S3 Craft plugin.

A benefit of using the Amazon S3 plugin is trivial integration with Cloudfront, a CDN that caches and distributes your images globally.

There are also Craft plugins for Google Cloud Storage, DigitalOcean Spaces, and Azure Blob Storage.

Caching #

Craft’s general-purpose data caches are stored in the file system by default, so you will run into the same issues as PHP’s file-based sessions and assets. However, the solutions are similar; you can configure Craft to store its caches in the databaseRedis or Memcached.

securityKey Config Setting #

Every web server should have the same Craft securityKey config setting set to ensure all web servers can decrypt any encrypted database contents. If you are using .env, this can be done with a CRAFT_SECURITY_KEY variable or a PHP constant with the same name.

storage Folder #

Craft’s storage folder is used for files like database backups, logs, and custom site logos. You should point that folder to a shared storage location, like an NFS mount, using the CRAFT_STORAGE_PATH PHP constant so all web servers can access it.

cpresources Folder #

Craft will generate the control panel resources it needs on demand, so using common storage for this folder is not required. Be sure that all missing file requests are routed to index.php, however, otherwise some of these may 404.

You can set Craft 4’s buildId config setting to a git SHA or deployment timestamp to ensure cache headers are set properly even as a rolling deployment is in progress.

Database #

It’s common to have multiple web servers interact with a single, high-performance database server, but database traffic may also be load-balanced.

One way to load balance database traffic is to utilize read/write splitting. This involves having multiple database instances where you can configure Craft to send all read queries to one (or more) database instances, and all write queries to a different database instance. The write queries will eventually propagate any changes to all of the read instances behind the scenes.

You’ll want to allocate database servers to suit the site’s operations—most commonly one for writing and several for reading, since the majority of traffic fetches details from the database.

Project Config #

Craft’s Project Config writes YAML files that are important for managing its configuration. We recommend disabling admin changes outside local development environments and certainly in multi-server production environments. You should strictly disable allowAdminChanges or disable writing YAML files altogether so web servers can’t write independent changes that lead to conflicts.

Logging #

By default, Craft will log to a file in the storage path. Since many servers in a load-balanced environment potentially mean lots of logging, we recommend collecting these logs in one place. If you’re using a container-based system such as AWS ECS or Kubernetes, it’s best to configure Craft to set the CRAFT_STREAM_LOG constant to true in order to send log output to STDOUT and STDERR.

Health Checks #

Health checks are a key part of load-balanced applications. A health check is designed to ensure all your app’s necessary services are available before the load balancer sends traffic to the server. Depending on your setup, these services may include a database or required environment variables, for example.

Because load-balanced web servers are managed automatically, it’s essential that a health check verifies the minimum requirements for each to operate with Craft before handling traffic. Every Craft site will require its database at minimum.

Since Craft’s plugin architecture depends on Craft, it’s best to perform the health check outside of Craft, but using the same environment variables/database configuration that Craft is using.

Load balancers run health checks frequently, so it’s important to keep checks simple. For example, this sample script ensures a database connection exists and outputs success or error:

<?php

try {
    (new PDO(
        'mysql:host=' . getenv('CRAFT_DB_SERVER'),
        getenv('CRAFT_DB_USER'),
        getenv('CRAFT_DB_PASSWORD')
    ))->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    echo 'success';
} catch (PDOException $e) {
    error_log($e->getMessage());

    echo 'error';
}

Health Check Endpoint #

If you’re using Craft 3.5.6+, a health check endpoint is available at /actions/app/health-check. This returns a 200 HTTP response with an empty body as long as Craft is running normally—even if an update is pending.

The health check was added with load-balanced production infrastructure in mind, so its focus is on application readiness and not application state.

The health check will fail if one or more of the following is true…

  • Craft’s System Status is set to “Offline”
  • Craft can’t connect to the database
  • Craft is not installed
  • Craft’s system requirements are not met
  • an application error or exception is thrown before Craft can return its 200 response

Applies to Craft CMS 4 and Craft CMS 3.