web php
Session Lifecycle
A PHP session is not active for the whole life of a browser tab. It is loaded, used, written, and closed during each request.
The usual flow is: PHP receives a session ID cookie, finds the matching server-side session record, fills $_SESSION, your code reads or changes it, then PHP writes the final data back near the end of the request. Understanding that flow helps debug missing session data, slow pages that block other tabs, login/logout behaviour, and flash messages that disappear at the wrong time.
A session starts before you use $_SESSION
$_SESSION only represents the stored session after session_start() has run. Calling session_start() can also send a session cookie, so it must happen before output is sent.
<?php
declare(strict_types=1);
session_start();
if (!isset($_SESSION['visits'])) {
$_SESSION['visits'] = 0;
}
$_SESSION['visits']++;
echo 'Visit count: ' . $_SESSION['visits'] . PHP_EOL;
// Output on a first visit:
// Visit count: 1
In a real browser, the second request with the same session cookie would print Visit count: 2.
session_status() is useful when shared code needs to know whether a session has already started:
<?php
declare(strict_types=1);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
echo 'Session is ready.' . PHP_EOL;
// Prints:
// Session is ready.
Avoid putting session_start() deep inside random helper functions. Start the session deliberately near the edge of the request, before controllers or page scripts depend on it.
Session locking
Default file-based sessions are commonly locked while a request has the session open. The lock prevents two requests from the same user from writing conflicting session data at the same time.
That protection has a tradeoff. If one request starts the session and then does slow work, another request from the same browser may wait for the lock. This is why some pages feel slow only when the user opens another tab or triggers multiple AJAX requests.
If a request has finished reading or changing session data, close it before slow work:
<?php
declare(strict_types=1);
session_start();
$userId = $_SESSION['user_id'] ?? null;
session_write_close();
echo 'Session closed before exporting data for user ' . ($userId ?? 'guest') . PHP_EOL;
// Prints:
// Session closed before exporting data for user guest
session_write_close() writes the current session data and releases the lock. After closing, do not expect later changes to $_SESSION in the same request to be saved.
For read-only pages, PHP also supports read_and_close:
<?php
declare(strict_types=1);
session_start(['read_and_close' => true]);
$userId = $_SESSION['user_id'] ?? null;
echo $userId === null ? 'Guest page' : 'Member page';
// Prints:
// Guest page
Use this only when the request genuinely does not need to change session data. A page that consumes a flash message, updates a cart, or refreshes login state still needs a writable session.
Flash messages use the lifecycle
Flash messages are stored for the next request and then removed. That is a lifecycle pattern: write during one request, read and delete during the next.
<?php
declare(strict_types=1);
$session = [
'flash' => ['Profile updated.'],
];
$messages = $session['flash'] ?? [];
unset($session['flash']);
foreach ($messages as $message) {
echo $message . PHP_EOL;
}
// Prints:
// Profile updated.
In a web request, $session would usually be $_SESSION. The important detail is that the message is removed after reading so it does not show forever.
Logout lifecycle
Logging out should clear session data, destroy the server-side session, and expire the session cookie in the browser.
<?php
declare(strict_types=1);
session_start();
$_SESSION = [];
$cookie = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $cookie['path'],
'domain' => $cookie['domain'],
'secure' => $cookie['secure'],
'httponly' => $cookie['httponly'],
'samesite' => $cookie['samesite'] ?? 'Lax',
]);
session_destroy();
echo 'Logged out.' . PHP_EOL;
// Prints:
// Logged out.
The cookie must be expired using the same path and domain that were used to create it. Otherwise the browser may keep the old session ID cookie.
Common lifecycle mistakes
Starting output before session_start() can stop PHP from sending session headers. That often appears as "sessions do not work" when the real issue is early output from whitespace, debugging text, or an included file.
Changing $_SESSION after session_write_close() does not persist those later changes. If you need to update the session again, rethink the request flow instead of reopening sessions in scattered places.
Long-running requests should not keep the session open longer than needed. Read the values you need, write any changes, close the session, then do slow work.
Logout should not only unset $_SESSION['user_id']. Clearing one key may leave old flash messages, role values, CSRF tokens, or other state behind. Treat logout as a full session reset.
What you should be able to do
After this lesson, you should be able to describe when session data is loaded and written, explain why session locks can slow concurrent requests, close a session before slow work, implement a simple flash-message lifecycle, and clear session state correctly on logout.
Practice
Task: Trace A Session Lifecycle
Build a small PHP script that shows how session data moves through a request lifecycle.
Requirements
- Use
declare(strict_types=1);. - Represent session data with an array so the script can run from the command line.
- Create a function that reads flash messages and removes them after reading.
- Create a function that prepares a logout by clearing the session and returning the cookie expiry options that would be sent.
- Include one normal case where a flash message is consumed.
- Include one edge case where there are no flash messages.
- Print enough output to prove the lifecycle behaviour.
Check Your Work
Run the script and confirm that flash messages are only returned once, the empty case returns an empty list, and the logout step leaves the session array empty.
Show solution
One way to solve it is to model the parts of $_SESSION that matter. The functions are written against arrays so the lifecycle can be tested without a browser.
<?php
declare(strict_types=1);
/**
* @param array<string, mixed> $session
* @return list<string>
*/
function consumeFlashMessages(array &$session): array
{
$messages = $session['flash'] ?? [];
if (!is_array($messages)) {
$messages = [];
}
unset($session['flash']);
return array_values(array_filter(
$messages,
static fn (mixed $message): bool => is_string($message) && $message !== ''
));
}
/**
* @param array<string, mixed> $session
* @return array{expires: int, path: string, secure: bool, httponly: bool, samesite: string}
*/
function prepareLogout(array &$session): array
{
$session = [];
return [
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
];
}
$session = [
'user_id' => 42,
'flash' => ['Saved changes.'],
];
$firstRead = consumeFlashMessages($session);
$secondRead = consumeFlashMessages($session);
$expiredCookie = prepareLogout($session);
echo 'First flash read: ' . implode(', ', $firstRead) . PHP_EOL;
echo 'Second flash count: ' . count($secondRead) . PHP_EOL;
echo 'Session values after logout: ' . count($session) . PHP_EOL;
echo 'Cookie expires in past: ' . ($expiredCookie['expires'] < time() ? 'yes' : 'no') . PHP_EOL;
// Prints:
// First flash read: Saved changes.
// Second flash count: 0
// Session values after logout: 0
// Cookie expires in past: yes
In a real request, $session would be $_SESSION. The same logic still applies: read flash messages once, remove them after reading, and clear all session state during logout.
Why This Works
The normal case proves that a flash message survives until the next request. The second read proves that it is removed after being consumed. The logout function proves that application state is cleared and that the browser would receive an expired session cookie.