# Save PHP Sessions to MySQL Database (Custom SessionHandlerInterface)

> How to persist PHP sessions in a MySQL database using a custom PDO SessionHandlerInterface implementation for multi-node or managed WordPress hosts.

- Canonical URL: <https://carlossura.com/blog/save-php-sessions-to-database/>
- Author: Carlos Sura
- Published: 2020-05-15
- Updated: 2026-10-20
- Tags: php, sessions, support, database, save, mysql, wordpress

When running PHP or WordPress workloads on load-balanced servers or managed hosting platforms like [WP Engine](https://wpengine.com/support/cookies-and-php-sessions), default filesystem-based PHP sessions (`/var/lib/php/sessions`) often fail because consecutive HTTP requests can be routed to different application nodes or stripped by edge page caching.

While in-memory stores like **Redis** are ideal for high-throughput clusters, persisting PHP sessions in **MySQL** using PHP's built-in `SessionHandlerInterface` provides a zero-dependency solution when a dedicated Redis instance is unavailable.

> **Disclaimer:** Evaluate your concurrency requirements before storing high-volume session data in a relational database. You can adapt the PDO handler below to your environment and review the official PHP references at the end of this guide.

---

## Create the MySQL Sessions Table (`utf8mb4` + Expiration Index)

Create a dedicated `sessions` table using `InnoDB` and `utf8mb4`. Adding a secondary index on the `access` timestamp column (`idx_access`) is critical so garbage collection (`DELETE FROM sessions WHERE access < ?`) uses an index range scan instead of locking the entire table:

```sql
CREATE DATABASE IF NOT EXISTS `wp_sessions`
  DEFAULT CHARACTER SET utf8mb4
   COLLATE utf8mb4_unicode_ci;

USE `wp_sessions`;

CREATE TABLE `sessions` (
  `id` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,
  `access` int unsigned NOT NULL,
  `data` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_access` (`access`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## Implement `SessionHandlerInterface` with PDO

A complete custom session handler in PHP implements six lifecycle methods defined by `SessionHandlerInterface`:

1. **`open`**: Initializes the PDO database connection.
2. **`read`**: Fetches the serialized session payload by session ID.
3. **`write`**: Performs an atomic upsert (`INSERT ... ON DUPLICATE KEY UPDATE` or `REPLACE INTO`) with the current Unix timestamp.
4. **`destroy`**: Deletes a specific session row when `session_destroy()` is invoked.
5. **`close`**: Releases the PDO database connection handle.
6. **`gc`**: Purges expired session rows older than `$lifetime` seconds.

Save the following class as `sessionhandler.class.php`:

```php
<?php

class WpSessionHandler implements SessionHandlerInterface
{
    private ?PDO $db = null;

    public function open(string $savePath, string $sessionName): bool
    {
        $host   = getenv('SESSION_DB_HOST') ?: 'localhost';
        $dbname = getenv('SESSION_DB_NAME') ?: 'wp_sessions';
        $user   = getenv('SESSION_DB_USER') ?: 'user';
        $pass   = getenv('SESSION_DB_PASS') ?: 'password';

        try {
            $dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $host, $dbname);
            $this->db = new PDO($dsn, $user, $pass, [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
                PDO::ATTR_EMULATE_PREPARES   => false,
            ]);
            return true;
        } catch (PDOException $e) {
            error_log('Session DB connection failed: ' . $e->getMessage());
            return false;
        }
    }

    public function read(string $id): string|false
    {
        if (!$this->db) {
            return '';
        }

        $query = $this->db->prepare('SELECT data FROM sessions WHERE id = ? LIMIT 1;');
        $query->execute([$id]);
        $row = $query->fetch();

        return $row === false ? '' : (string) $row->data;
    }

    public function write(string $id, string $data): bool
    {
        if (!$this->db) {
            return false;
        }

        $access = time();
        $sql = 'INSERT INTO sessions (id, access, data) VALUES (?, ?, ?) '
             . 'ON DUPLICATE KEY UPDATE access = VALUES(access), data = VALUES(data);';

        $query = $this->db->prepare($sql);
        return $query->execute([$id, $access, $data]);
    }

    public function destroy(string $id): bool
    {
        if (!$this->db) {
            return false;
        }

        $query = $this->db->prepare('DELETE FROM sessions WHERE id = ?;');
        return $query->execute([$id]);
    }

    public function close(): bool
    {
        $this->db = null;
        return true;
    }

    public function gc(int $max_lifetime): int|false
    {
        if (!$this->db) {
            return false;
        }

        $expiration = time() - $max_lifetime;
        $query = $this->db->prepare('DELETE FROM sessions WHERE access < ?;');
        $query->execute([$expiration]);

        return $query->rowCount();
    }
}
```

---

## Register the Handler & Bypass Reverse-Proxy Caching

Initialize the session handler inside your WordPress plugin or application bootstrap before any output is sent:

```php
require_once __DIR__ . '/sessionhandler.class.php';

$handler = new WpSessionHandler();
session_set_save_handler($handler, true);

// Use a custom session cookie name and configure secure cookie flags
session_name('my_session_name');
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();
```

If you are hosting on **WP Engine** or behind **Cloudflare / Varnish**, make sure the edge cache is configured to bypass or vary on your custom cookie name (`my_session_name`) for routes that depend on active user sessions.

## References

- [PHP Manual: SessionHandlerInterface](https://www.php.net/manual/en/class.sessionhandlerinterface.php)
- [PHP Manual: Custom Session Handlers (`session_set_save_handler`)](https://www.php.net/manual/en/function.session-set-save-handler.php)
- [WP Engine: Cookies and PHP Sessions](https://wpengine.com/support/cookies-and-php-sessions)
- [Stack Overflow: Saving PHP Session Data to a Database](https://stackoverflow.com/questions/36753513/how-do-i-save-php-session-data-to-a-database-instead-of-in-the-file-system)
