PHP Sessions

Su Subhalakshmi Chandran Updated 17 Oct 2026
7 min read ·Lesson 66 of 109
In this post

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)
ConstantMeaning
PHP_SESSION_DISABLEDSessions are disabled on this server
PHP_SESSION_NONESessions are available, but none is started
PHP_SESSION_ACTIVEA 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,
]);
SettingRecommendedPurpose
nameA custom name, e.g. __Host-SIDHides the default PHPSESSID and enables the __Host- cookie protections
cookie_lifetime0Cookie ends when the browser closes
cookie_secure / cookie_httponly / cookie_samesitetrue / true / LaxProtects the session cookie (see PHP Cookies)
use_strict_modetrueRejects session IDs that the server did not create
use_only_cookiestrueNever accepts session IDs from URLs
gc_maxlifetimeDepends on your app (e.g. 7200)Inactive sessions older than this may be deleted
sid_length, sid_bits_per_characterDefaultsDeprecated 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

  1. On the first request, both timestamps are initialized.
  2. If more than 30 minutes passed since the last activity, the session has expired due to inactivity.
  3. If the session is older than 8 hours, it has expired regardless of activity.
  4. 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() during session_start(), write() at the end of the request, destroy() on session_destroy(), and gc() during garbage collection.
  • $data is the serialized session; PHP handles serialization, so the handler only stores strings.
  • session_set_save_handler($handler, true) registers the handler; true ensures 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

FunctionPurpose
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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about PHP Sessions and get answers drawn from it.

Signed-in readers only.