A visitor ticks "remember me", comes back the next morning, and the site has forgotten them. The dark theme they picked resets on the next click. HTTP remembers nothing between requests, so you need somewhere to park a scrap of text. PHP cookies are that somewhere: a name, a value and a date, handed back to you on every request after.
The part that catches people is timing. setcookie() does not write to a variable you can read. It adds a header to a response you have not finished sending, so a cookie you set on line three is not in $_COOKIE on line four. Get that straight and most cookie bugs explain themselves.
What PHP cookies actually are
A cookie is not a PHP feature. It is a pair of HTTP headers. Your script sends Set-Cookie: username=Flow, the browser writes it down, and on every later request it sends Cookie: username=Flow back. PHP unpacks that incoming header into $_COOKIE before your first line runs.
So setcookie() is a header-writing function, nothing more. This page sets a cookie, asks whether $_COOKIE knows about it, then prints the headers about to be sent.
<?php
setcookie('username', 'Flow', time() + 3600);
// on this request the cookie is not here yet
var_dump(isset($_COOKIE['username']));
// but the instruction to store it is already in the response
print_r(headers_list());
Output:
bool(false), because the browser sent no Cookie header at all. It had nothing to send yet. And a Set-Cookie line waiting in the outgoing headers, which is the instruction the browser is about to obey. The cookie is a promise, not data.
Reading PHP cookies with $_COOKIE
$_COOKIE is an ordinary superglobal array, built once at the start of the request from the header the browser sent. Keys are cookie names, values are always strings, even when you set a number. Load a second page and the value is finally there.
<?php
if (isset($_COOKIE['username'])) {
echo 'Welcome back, ' . htmlspecialchars($_COOKIE['username']) . "\n";
} else {
echo "No username cookie came with this request\n";
}
print_r($_COOKIE);
Output:
Note the htmlspecialchars(). Everything in $_COOKIE came from the browser, so it is user input, the same as a form field. Reach for isset() or $_COOKIE['username'] ?? 'guest' rather than the bare key: reading an absent key raises a warning.
setcookie() must come before any output
PHP cookies are headers, and headers travel ahead of the body. Once one byte of body has gone โ an echo, a line of HTML above the opening tag, a blank line after a closing ?> in an include โ the headers went with it.
<?php
echo "Setting a cookie...\n"; // this line sends the body
setcookie('theme', 'dark', time() + 3600);
echo "Did it work?\n";
Output:
The warning names the file and line where output started, which is the useful half: it points at the leak, not at the cookie. setcookie() returns false rather than throwing, so a script that ignores the return value never sets the cookie and never says so.
This bug travels badly too: if output_buffering is on in php.ini โ XAMPP ships it at 4096 bytes โ PHP holds the body back, the headers stay open, and the same call succeeds with no warning until the buffer fills. The fix is layout, not configuration. Do the cookie work at the top of the file and print afterwards.
Expiry is a Unix timestamp
The third argument is not a number of seconds and not a date string. It is an absolute Unix timestamp: the moment the cookie should die, counted in seconds since 1 January 1970. That is why examples add to time().
Leave the argument out and you get a session cookie, dropped when the browser closes. That is the right choice more often than people assume.
<?php
$hour = time() + 3600;
$week = time() + (7 * 24 * 60 * 60);
setcookie('one_hour', 'short', $hour);
setcookie('one_week', 'longer', $week);
setcookie('until_close', 'no expiry argument at all');
echo 'time() right now: ' . time() . "\n";
echo 'one hour from now: ' . $hour . ' ' . date('D, d M Y H:i:s', $hour) . "\n";
echo 'one week from now: ' . $week . ' ' . date('D, d M Y H:i:s', $week) . "\n";
Output:
Passing 3600 on its own is the classic slip. That timestamp lands in January 1970, so the browser discards the cookie the instant it arrives. The expiry is a request, not a guarantee: the user can clear it whenever they like.
Deleting a PHP cookie
There is no unsetcookie(). You delete a cookie by setting it again with an expiry in the past, which tells the browser to throw its copy away.
<?php
setcookie('username', '', time() - 3600);
// the browser has been told to drop it, but this request already had it
var_dump(isset($_COOKIE['username']));
print_r(headers_list());
Output:
bool(true), still, and that is correct. $_COOKIE holds what arrived at the start of this request, and the browser sent the cookie because it had not been told otherwise yet. If the page must also act as though it has gone, add unset($_COOKIE['username']).
Deletion only works when path and domain match the ones the cookie was created with; delete with defaults a cookie set with path => '/' and you write a second cookie while the original sits untouched.
path, domain and the options array
path and domain decide which requests carry the cookie back. Leave path out and the browser defaults it to the directory of the current script, so a cookie set by /shop/checkout.php never reaches /index.php. Pass '/' for the whole site. Leave domain out and the cookie belongs to the host that set it; name one and its subdomains come too.
The positional form ends at secure and httponly, with no room for samesite. PHP 7.3 added a second signature taking an options array, the only way to set it.
<?php
// positional: name, value, expires, path, domain, secure, httponly
setcookie('theme', 'dark', time() + 86400, '/', '', false, true);
// PHP 7.3 and newer: the same cookie as an options array, plus samesite
setcookie('theme', 'dark', [
'expires' => time() + 86400,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
print_r(headers_list());
Output:
Both headers go out and the second wins. Three flags decide how safely PHP cookies travel. httponly hides the value from document.cookie, so JavaScript injected by an attacker cannot read it. secure stops the browser sending it over plain HTTP, and stops it being stored at all on http://localhost, so expect that second cookie to vanish on XAMPP. samesite decides whether the cookie rides along on requests started by another site: 'Lax' is the sensible default, 'Strict' tighter, and 'None' rejected unless secure is true.
PHP cookies versus sessions
Both survive between requests, but keep the data in opposite places. A cookie lives in the browser, in a file the user owns, so they can open it, edit it and send back whatever they like. A session lives on your server; the browser only gets the id.
<?php
session_start();
$_SESSION['role'] = 'admin'; // kept in a file on the server
setcookie('role', 'admin'); // kept in the browser, in plain text
print_r(headers_list());
echo "\n";
echo 'Session id the browser will hold: ' . session_id() . "\n";
echo 'Role the server will hold: ' . $_SESSION['role'] . "\n";
Output:
The header says role=admin in plain text, and anyone can edit that word and reload. The session sends a random id and keeps admin out of reach. So cookies carry preferences you would not mind a user rewriting โ a theme, a language, a dismissed banner โ and anything you make a decision on belongs in a PHP session.
Three mistakes with PHP cookies
Calling setcookie() after the HTML has started. One echo, one blank line before <?php, one include ending in whitespace after ?>, and the headers are gone: "Cannot modify header information โ headers already sent". The cookie is silently not set, and the line reported is the output, not the cookie. Move every header call above the first byte of markup.
Expecting $_COOKIE to update in the same request. $_COOKIE is a snapshot of what arrived, not a live view of what the browser holds. Read it back two lines after setting it and you get the old value or none, and the code looks right because it is only wrong on the first request. If the page needs the value now, hold it in a variable too.
Storing a user id or a role in a plain cookie and trusting it. setcookie('user_id', 42) then if ($_COOKIE['role'] === 'admin') on the next page is a login anyone defeats by editing two words. The value is not signed, not encrypted, and not yours once it leaves the server. Put the id in a session instead.
Common questions about PHP cookies
How do I set a cookie in PHP?
setcookie('name', 'value', time() + 3600, '/'); before any output. The third argument is the expiry as a Unix timestamp; the fourth is the path, which you nearly always want to be '/' so the cookie reaches every page.
Why is my cookie not showing in $_COOKIE?
Three causes, in order of likelihood. You are checking on the request that set it, so the browser has not sent it back yet: reload. Or output started before setcookie() ran, so the header never left. Or the path does not cover the page you are on.
How do I delete PHP cookies?
Set the same name again with an expiry in the past, such as time() - 3600, using the same path and domain as the original. Add unset($_COOKIE['name']) if the rest of that request must behave as though it has already gone.
What is the difference between cookies and sessions in PHP?
A cookie stores the data in the browser, where the user can read and change it. A session stores it on the server and puts only an id in the browser. Cookies are for preferences; sessions are for anything the application acts on.
How long do PHP cookies last?
Until the timestamp you pass as the third argument, or until the browser closes if you pass nothing. There is no maximum you can rely on: browsers cap long lifetimes themselves.
What is next
PHP cookies are the first place user-controlled data walks straight back into your code, and not the last: form fields, query strings and uploads arrive the same way. PHP security covers what to do about it: escaping output, validating input, prepared statements and password hashing.
Loading comments...