PHP Sessions
The $_SESSION lesson introduced sessions: starting them, storing values, logging users in and out, and flash messages. This lesson looks at how sessions work internally and how to run them reliably in
The $_SESSION lesson introduced sessions: starting them, storing values, logging users in and out, and flash messages. This lesson looks at how sessions work internally and how to run them reliably in real applications: the session lifecycle, configuration, timeouts, locking, and storing sessions in a database or Redis.
The Session Lifecycle
Request arrives with cookie PHPSESSID=abc123
│
▼
session_start()
├── open the storage
├── read the data for "abc123" (and lock it)
└── unserialize it into $_SESSION
│
▼
Your script reads and modifies $_SESSION
│
▼
End of script (or session_write_close())
├── serialize $_SESSION
├── write it back for "abc123"
└── release the lock
Occasionally, session_start() also triggers garbage collection, which deletes sessions that have not been used for longer than session.gc_maxlifetime seconds.
Checking the Session Status
<?php
function ensureSession(): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
var_dump(session_status() === PHP_SESSION_NONE);
ensureSession();
var_dump(session_status() === PHP_SESSION_ACTIVE);
ensureSession(); // safe to call again
Output
bool(true)
bool(true)
| Constant | Meaning |
|---|---|
PHP_SESSION_DISABLED | Sessions are disabled on this server |
PHP_SESSION_NONE | Sessions are available, but none is started |
PHP_SESSION_ACTIVE | A session is active |
Calling session_start() twice triggers a notice; ensureSession() avoids this in code that may run in different contexts.
Configuring Sessions
Session behavior is controlled by session.* settings, which can be set in php.ini or passed as an options array to session_start() (without the session. prefix):
<?php
session_start([
'name' => '__Host-SID',
'cookie_lifetime' => 0,
'cookie_path' => '/',
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
'use_only_cookies' => true,
'gc_maxlifetime' => 7200,
]);
| Setting | Recommended | Purpose |
|---|---|---|
name | A custom name, e.g. __Host-SID | Hides the default PHPSESSID and enables the __Host- cookie protections |
cookie_lifetime | 0 | Cookie ends when the browser closes |
cookie_secure / cookie_httponly / cookie_samesite | true / true / Lax | Protects the session cookie (see PHP Cookies) |
use_strict_mode | true | Rejects session IDs that the server did not create |
use_only_cookies | true | Never accepts session IDs from URLs |
gc_maxlifetime | Depends on your app (e.g. 7200) | Inactive sessions older than this may be deleted |
sid_length, sid_bits_per_character | Defaults | Deprecated in PHP 8.4; leave them unset |
Applying the same settings in php.ini (for example session.cookie_secure = 1) makes them the default for the whole server.
Session Timeouts
gc_maxlifetime is not a precise timeout: garbage collection runs only occasionally, and some systems handle it differently. Applications that need reliable timeouts store timestamps in the session and check them on every request.
Two kinds of timeout are common:
- Idle timeout — log out after a period of inactivity (for example, 30 minutes).
- Absolute timeout — log out after a maximum session age, even if the user is active (for example, 8 hours).
<?php
function checkSessionAge(array &$session, int $now, int $idleLimit, int $absoluteLimit): string
{
$session['created_at'] ??= $now;
$session['last_activity'] ??= $now;
if ($now - $session['last_activity'] > $idleLimit) {
return 'expired: idle';
}
if ($now - $session['created_at'] > $absoluteLimit) {
return 'expired: too old';
}
$session['last_activity'] = $now;
return 'valid';
}
$idle = 30 * 60; // 30 minutes
$absolute = 8 * 3600; // 8 hours
$session = ['created_at' => 10_000, 'last_activity' => 10_000];
echo checkSessionAge($session, 10_600, $idle, $absolute) . "\n"; // 10 minutes later
echo checkSessionAge($session, 13_000, $idle, $absolute) . "\n"; // 40 minutes after last activity
$longSession = ['created_at' => 10_000, 'last_activity' => 38_000];
echo checkSessionAge($longSession, 39_000, $idle, $absolute) . "\n";
Output
valid
expired: idle
expired: too old
How It Works
- On the first request, both timestamps are initialized.
- If more than 30 minutes passed since the last activity, the session has expired due to inactivity.
- If the session is older than 8 hours, it has expired regardless of activity.
- Otherwise, the activity timestamp is updated.
In a real application, checkSessionAge($_SESSION, time(), ...) runs at the start of every request; on expiry, the application destroys the session and redirects to the login page. The function receives the session array by reference and the current time as a parameter, so it can be tested with fixed numbers, as shown.
Regenerating the ID Periodically
Besides regenerating the session ID at login, regenerating it periodically (for example, every 15 minutes) limits how long a stolen ID remains useful:
<?php
if (($_SESSION['regenerated_at'] ?? 0) < time() - 900) {
session_regenerate_id(true);
$_SESSION['regenerated_at'] = time();
}
Session Locking and Concurrency
With the default file storage, session_start() locks the session until the script ends. Requests from the same user are therefore processed one after another:
Request A (slow report, 8 s) ──── holds the session lock ─────────►
Request B (simple AJAX call) waits ......................... ► runs
Releasing the Lock Early
<?php
session_start();
$userId = $_SESSION['user_id'] ?? null;
session_write_close(); // save changes and release the lock
// long-running work that does not modify the session
Read-Only Sessions
<?php
session_start(['read_and_close' => true]);
echo $_SESSION['user_name'] ?? 'Guest';
read_and_close reads the session data and immediately releases the lock. Changes to $_SESSION afterwards are not saved. Use it for pages and API endpoints that only need to know who the user is.
Storing Sessions Elsewhere
Why Change the Storage?
The default storage keeps session files on the local disk of one server. When an application runs on several servers behind a load balancer, each request may reach a different server, which would not find the session file. Shared storage solves this.
Redis
With the phpredis extension installed, sessions can be stored in Redis by configuration alone:
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
Redis is fast, shared between servers, and expires old sessions automatically.
A Database Session Handler
PHP lets you implement your own storage through SessionHandlerInterface:
<?php
final class PdoSessionHandler implements SessionHandlerInterface
{
public function __construct(private PDO $pdo) {}
public function open(string $path, string $name): bool
{
return true;
}
public function close(): bool
{
return true;
}
public function read(string $id): string|false
{
$stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id = ?');
$stmt->execute([$id]);
return (string) ($stmt->fetchColumn() ?: '');
}
public function write(string $id, string $data): bool
{
$stmt = $this->pdo->prepare(
'REPLACE INTO sessions (id, data, last_activity) VALUES (?, ?, ?)'
);
return $stmt->execute([$id, $data, time()]);
}
public function destroy(string $id): bool
{
return $this->pdo->prepare('DELETE FROM sessions WHERE id = ?')->execute([$id]);
}
public function gc(int $max_lifetime): int|false
{
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE last_activity < ?');
$stmt->execute([time() - $max_lifetime]);
return $stmt->rowCount();
}
}
// Usage (before session_start):
// session_set_save_handler(new PdoSessionHandler($pdo), true);
// session_start();
With this table (MySQL):
CREATE TABLE sessions (
id VARCHAR(128) NOT NULL PRIMARY KEY,
data MEDIUMBLOB NOT NULL,
last_activity INT UNSIGNED NOT NULL,
INDEX (last_activity)
);
How It Works
- PHP calls the handler's methods at the appropriate times:
read()duringsession_start(),write()at the end of the request,destroy()onsession_destroy(), andgc()during garbage collection. $datais the serialized session; PHP handles serialization, so the handler only stores strings.session_set_save_handler($handler, true)registers the handler;trueensures the session is written when the script shuts down.
Note that this simple handler does not lock sessions, so concurrent requests from the same user can overwrite each other's changes. Production-ready handlers (for example, Symfony's PdoSessionHandler) implement locking. Database sessions also make it possible to show users their active sessions and to log out a user on all devices.
Useful Session Functions
| Function | Purpose |
|---|---|
session_start($options) | Start or resume a session |
session_status() | Current status |
session_id() / session_name() | Current session ID / cookie name |
session_regenerate_id(true) | New ID; delete the old session data |
session_write_close() | Save and release the lock |
session_destroy() | Delete the session data from storage |
session_get_cookie_params() | Current cookie settings |
session_set_save_handler() | Use custom storage |
Common Mistakes
Relying on gc_maxlifetime for Timeouts
Effect: Sessions survive longer than intended, or are deleted unpredictably.
Fix: Implement idle and absolute timeouts with timestamps.
Holding the Lock During Slow Work
Effect: Other requests from the same user wait, and pages feel slow.
Fix: Call session_write_close() early, or use read_and_close.
Using Local File Sessions on Multiple Servers
Effect: Users are randomly logged out when requests reach different servers.
Fix: Use shared storage (Redis or a database).
Storing Large Data in Sessions
Effect: Every request reads and writes the whole session, slowing the application.
Fix: Store identifiers in the session and load data when needed.
Accepting Session IDs From URLs
Effect: Session IDs leak through links, logs, and Referer headers.
Fix: Keep use_only_cookies enabled and use_trans_sid disabled.
Best Practices
- Configure secure cookie settings and strict mode before starting sessions.
- Implement explicit idle and absolute timeouts, and regenerate IDs at login and periodically.
- Release session locks as soon as the session is no longer modified.
- Use Redis or a database for sessions in multi-server deployments.
- Keep session data small and free of secrets.
Official reference: https://www.php.net/manual/en/book.session.php