PHP Sessions and Cookies: Keeping Users Logged In Safely
What are sessions and cookies in PHP?
Cookies and sessions are how PHP remembers a visitor between requests. A cookie is a small piece of data the browser stores and sends back with every request. A session keeps data on the server in $_SESSION and links it to the visitor through a single random session ID, which is itself stored in a cookie.
Why HTTP needs cookies and sessions
HTTP is stateless: each request arrives on its own, and the server has no built-in memory that the same person sent the previous one. Logins, shopping carts and language preferences all need some way to connect requests together.
| Cookie | Session | |
|---|---|---|
| Where data lives | In the browser | On the server |
| Can the visitor read or edit it? | Yes | No, only the session ID |
| Typical size | A few kilobytes at most | Limited only by server storage |
| Good for | Preferences, consent choices, the session ID | User ID after login, cart contents, flash messages |
The rule that follows is simple: never trust a cookie value for anything that matters, because the visitor controls it. Store important state in the session instead.
How setcookie() and $_COOKIE work
Cookies are sent as HTTP headers, so setcookie() must run before any HTML or whitespace is output. Since PHP 7.3 you can pass an options array, which is clearer than the long list of positional arguments.
<?php
setcookie('theme', 'dark', [
'expires' => time() + 60 * 60 * 24 * 30, // 30 days
'path' => '/',
'secure' => true, // only sent over HTTPS
'httponly' => true, // hidden from JavaScript
'samesite' => 'Lax', // Strict, Lax or None
]);
$theme = $_COOKIE['theme'] ?? 'light';
// Delete a cookie by expiring it with the same path
setcookie('theme', '', ['expires' => time() - 3600, 'path' => '/']);securestops the cookie travelling over plain HTTP.SameSite=Noneonly works together withsecure.httponlystops scripts reading the cookie, which limits the damage of a cross-site scripting bug.samesitecontrols whether the cookie is sent on requests from other sites, which helps against cross-site request forgery.- A new cookie appears in
$_COOKIEonly on the next request, not the one that set it.
How PHP sessions work behind the scenes
When you call session_start(), PHP looks for a session ID cookie (named PHPSESSID by default). If it finds one, it loads the matching data into $_SESSION; if not, it creates a new random ID and sends it as a cookie. At the end of the request PHP saves $_SESSION back to storage, which is a file on the server unless you configure a database or cache.
<?php
session_start([
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
]);
$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
echo 'Visits this session: ' . $_SESSION['visits'];Because the session ID is the only key to the data, anyone who steals it can act as that user. That is why the cookie flags matter, and why the site must run over HTTPS.
On a live server, set these options once in php.ini or your framework configuration rather than on every page, so no page can forget them. If your application runs on more than one server behind a load balancer, file-based sessions will not be shared between machines; store them in a database or a cache such as Redis so every server sees the same session data. Keep $_SESSION small and store IDs rather than whole database rows, which can go stale.
Logging in, flash messages and logging out
A secure login stores only the user ID in the session and issues a brand-new session ID at the moment of login.
<?php
// login.php, after password_verify() succeeds
session_regenerate_id(true); // new ID, old session deleted
$_SESSION['user_id'] = $user['id'];
$_SESSION['flash'] = 'Welcome back!';
header('Location: /dashboard');
exit;
// dashboard.php: show the flash message once, then remove it
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
// logout.php
$_SESSION = [];
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'],
]);
session_destroy();A proper logout does three things: it empties the session array, deletes the session cookie in the browser and destroys the stored data on the server. Make logout a POST request with a CSRF token, as explained in the PHP security guide, so other sites cannot log your users out.
Session fixation and how to prevent it
In a session fixation attack, an attacker gets a victim to use a session ID the attacker already knows, for example through a crafted link. When the victim logs in, the attacker shares the now-authenticated session. Two settings close this gap:
- Call
session_regenerate_id(true)whenever privilege changes: after login, after a password change and when a user gains admin rights. - Enable
session.use_strict_mode, so PHP refuses session IDs it did not create itself.
A related attack is session hijacking, where the ID is stolen after login rather than planted before it. HTTPS everywhere, the HttpOnly flag and escaping all output against cross-site scripting are your main defences.
Building remember-me login safely, step by step
A remember-me cookie keeps users signed in after the session ends. Never store a user ID or password in that cookie. Use a random token and keep only its hash in the database, so a leaked database cannot be used to log in.
- Create a table with columns for an ID, user ID, a
selector, atoken_hashand anexpires_atdate. - On login with the box ticked, generate
$selector = bin2hex(random_bytes(12))and$validator = bin2hex(random_bytes(32)). - Store the selector and
hash('sha256', $validator)in the table with an expiry date. - Send a cookie containing
selector:validatorwithsecure,httponlyandsamesiteset. - On a later visit with no session, look up the row by selector and compare hashes with
hash_equals(), which runs in constant time. - If it matches, regenerate the session ID, log the user in, then delete that row and issue a fresh token.
- On logout or password change, delete all of that user's tokens.
Practical exercise: a members area with flash messages
Build a three-page site on your own machine: login.php, members.php and logout.php. Hard-code one test user whose password is stored with password_hash().
- Start every page with the same
session_start()options shown above, kept in one included file. - Redirect visitors who are not logged in from
members.phpto the login page with a flash message. - Show "Logged in successfully" once after login and confirm it disappears on refresh.
- Add a theme cookie that switches between light and dark.
You are done when your browser developer tools show a different session ID before and after login, the session cookie carries the HttpOnly and SameSite flags, and after logout the back button no longer shows member content. The PHP foundation course uses this pattern in its project work.
Common mistakes
- Calling
setcookie()orsession_start()after HTML has been output, which causes a "headers already sent" warning. - Storing a user ID or role in a plain cookie and trusting it to decide who is logged in.
- Forgetting to regenerate the session ID after login, which leaves the site open to session fixation.
- Logging out with only
session_destroy(), leaving the session array and cookie behind. - Saving remember-me tokens in the database as plain text instead of as a hash.
- Putting large arrays or whole database rows in
$_SESSION, which slows every request and leaves stale data behind.
Frequently asked questions
How long does a PHP session last?
By default the session cookie lasts until the browser closes, and the server removes stored data after a period of inactivity controlled by session.gc_maxlifetime. For predictable expiry, track activity time in the session yourself.
Can I use sessions in an API?
You can for a browser-based app on the same site, but APIs called by mobile apps or other servers usually send a token in the Authorization header instead. Our guide to APIs covers token-based requests.
Do I need a cookie consent banner for session cookies?
Privacy rules in many regions treat strictly necessary cookies, such as a login session cookie, differently from analytics or advertising cookies. Check the rules that apply to your audience, and keep essential cookies separate from optional tracking ones.
Why does my page load slowly when two tabs are open?
With file-based storage, PHP locks a session while a request uses it, so a second request waits. Call session_write_close() as soon as you have finished changing $_SESSION to release the lock early.