Host Talk / Cookies, sessions and why logins fight with caches

Cookies, sessions and why logins fight with caches

HOST TALK

8 min read · 1,706 words

The web is stateless by design. Each request arrives with no memory of the previous one: the server answers it, forgets it, and meets the next request as a stranger. That is excellent for scaling and hopeless for anything involving a login, a basket or a language preference. Cookies are the patch. The server sends a small piece of text, the browser stores it and returns it with every later request, and the server uses it to recognise the visitor.

Cookies also sit in the middle of one of the most common support complaints in hosting: a site that is fast for anonymous visitors and sluggish for members, or, worse, a cache that shows one person's page to another. Both problems come from the same place, so it is worth understanding the mechanism before touching any setting.

A cookie is a name, a value and some attributes, and the attributes are where the interesting behaviour lives. The server creates it with a response header, and the browser files it against the domain that sent it. From then on, every request to that domain carries the cookie in a header, until it expires or is deleted.

HTTP/2 200
set-cookie: sid=9f3a1c0e7b52d8; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1209600

GET /account HTTP/2
Host: example.com
Cookie: sid=9f3a1c0e7b52d8

There are two broad lifetimes. A session cookie has no expiry date and disappears when the browser closes (in practice many browsers restore sessions, so "closes" is generous). A persistent cookie carries Max-Age or Expires and survives for that long. A single cookie is limited to roughly 4 KB, and browsers cap how many one domain may set, so a site that stuffs data into cookies eventually hits a wall and starts misbehaving in odd ways.

Browser Web server POST /login (username, password) 200 OK + Set-Cookie: sid=9f3a... GET /account + Cookie: sid=9f3a... The page for that one user
The server never remembers you; the browser reminds it, with the same token, on every request.

When you log in, a well-built site creates a session record on the server (in a file, a database table or a memory store such as Redis) and gives your browser a long random token. The token points to the record; the record says who you are, what is in your basket and when you last did anything. The cookie itself carries nothing worth stealing except the token, and a stolen token is only useful until the session expires or is revoked.

Some sites go the other way and put the data in the cookie, signed so it cannot be altered. That works, and it removes the server-side lookup, but it moves the risk: anything in the cookie is readable by the visitor, and revoking a signed cookie before it expires needs extra machinery. Either design is acceptable. What is never acceptable is a cookie holding a plain password, or a user ID with no signature that anyone can change from 1042 to 1043 and become somebody else.

Session length is a trade between convenience and risk. A shop might keep a basket for two weeks; a banking-style area might expire after fifteen minutes of inactivity. If members complain they are logged out constantly, the usual causes are a session lifetime shorter than they expect, session files being cleaned up by a server job, or the site flipping between example.com and www.example.com so the cookie set on one is never sent to the other.

The attributes that matter

AttributeWhat it doesIf it is missing
SecureCookie travels only over HTTPSSent in clear on any plain HTTP request, readable on shared Wi-Fi
HttpOnlyHidden from page scriptsInjected JavaScript can read the token and send it away
SameSiteLimits sending on cross-site requests (Lax, Strict, None)Browsers now treat a missing value as Lax, but older clients may not
Domain / PathWhich hosts and URLs receive itDefaults to the exact host that set it, which is the safer choice
Max-Age / ExpiresHow long it livesIt becomes a session cookie

If you build or review a site, the first three are the checklist. SameSite=Lax blunts a class of forged-request attacks, because a form on another site cannot make your browser submit a request with your login attached. SameSite=None exists for genuine cross-site uses such as an embedded widget, and it must be paired with Secure. Most sites should never need it.

Why this collides with caching

A cached page is the same for everyone. That is the entire point: build it once, hand out the copy a thousand times, and leave PHP and the database alone. A logged-in dashboard or a basket is the opposite, personal to one visitor. A cache has to tell them apart, and the usual signal is a cookie. If the request carries a login or cart cookie, skip the cache and let the application build a fresh page.

That rule is why a site can be lightning fast for anonymous visitors and sluggish for members. It is also why shops struggle most: the moment a visitor adds anything to a basket, a cart cookie appears and every page they visit afterwards bypasses the cache. Your busiest, most valuable visitors get the slowest experience.

Request arrives Does it carry a login or cart cookie? No Yes Serve the stored copy a few milliseconds Pass through to PHP and the database
The cookie rule most page caches apply, which is why members and shoppers see the slow path.

When the rule goes wrong

There are two failure directions, and they feel very different. The harmless one is a cache that bypasses too often: pages are slow, but correct. Plugins that set a cookie on every visitor, including anonymous ones, cause this. A marketing plugin that drops a tracking cookie on the first page view can disable page caching for the entire site, and the owner only notices when the server load graph climbs.

The harmful one is a cache that bypasses too rarely. If the cache keys only on the URL and ignores the cookie, the first logged-in visitor to load /account can cause that page to be stored and served to the next person. Real incidents of this kind involve a cached basket showing someone else's items, or a cached "Welcome back, Sam" appearing to strangers. It usually follows a hurried "cache everything" setting at a CDN or in a caching plugin.

If a response contains Set-Cookie, a shared cache should not store it. Check that your CDN rules do not strip or ignore that header on pages that vary by visitor, and test with two different browsers before declaring the setup finished.

The proper tools are Cache-Control: private on personal pages, which tells shared caches to keep out, and Vary: Cookie, which tells a cache to store separate copies per cookie value. The second is accurate but nearly useless on a busy site, since every visitor has a different cookie and the hit rate collapses. Most setups prefer a bypass rule for a short list of named cookies, such as wordpress_logged_in_ or woocommerce_items_in_cart.

Illustrative page generation time, milliseconds Anonymous, cached 30 Logged in, uncached 700 Logged in, object cache 300 Same site, same server; only the path through the stack differs.
Numbers are illustrative; the shape is typical, with logged-in pages an order of magnitude slower than cached ones.

Making logged-in pages faster without breaking them

If you cannot cache the whole page, cache the pieces. A persistent object cache (Redis or Memcached, where your host offers one) keeps the results of repeated database queries in memory, so a logged-in page still runs PHP but asks the database far fewer questions. Fragment caching stores the expensive part of a page, say a product listing, while leaving the personal part, a greeting or a basket count, to be built fresh. Some stacks go further and load the personal bits with a small JavaScript call after the cached page arrives.

On shared hosting, you generally get the first of these as a switch in the control panel and the third as a plugin option. On a VPS you can tune the cache rules yourself, for example telling Nginx to skip its FastCGI cache when particular cookies are present. Managed WordPress hosts usually ship reasonable defaults for the common cookie names, but a custom membership plugin with its own cookie name will not be on their list until you tell them.

Also reduce what sets cookies unnecessarily. Each plugin that starts a PHP session on every page (a call to session_start() is the giveaway) can turn caching off by itself, because PHP sends a session cookie in response. Removing or reconfiguring one such plugin sometimes does more for speed than a new server.

Cookies set by other domains, for advertising, analytics or embedded video, are the ones privacy law and browser changes target. Safari and Firefox block many of them by default, and Chrome has been moving more slowly in the same direction. Where privacy rules such as the GDPR and the ePrivacy rules apply, cookies that are not strictly necessary for the service the visitor asked for generally need consent before they are set. A login cookie normally counts as necessary; an advertising cookie does not.

The need for a consent banner on your site is a legal question for your own situation, and a banner that is purely decorative, while the scripts load regardless, solves nothing. What you can do yourself is find out which cookies your site actually sets. Most owners are surprised by the list.

A ten-minute check

You need nothing beyond a browser and a terminal. Start with what the server sends:

curl -sI https://example.com/ | grep -i -E 'set-cookie|cache-control|vary|x-cache|age'

An anonymous homepage that returns a Set-Cookie line is a warning sign for caching. Then repeat the request with a cookie, to see what changes:

curl -sI -H 'Cookie: wordpress_logged_in_abc=test' https://example.com/ | grep -i -E 'x-cache|cache-control'

Header names vary by host and CDN (x-cache, cf-cache-status, x-litespeed-cache), but a hit on the first request and a miss or bypass on the second is exactly what you want. In the browser, open developer tools, go to the Application tab (Storage in Firefox), pick Cookies and read down the list. Look at the Secure, HttpOnly and SameSite columns, and at the domain: anything not ending in your own domain is third party. Finally, open the site in a private window, log in as an ordinary user, and make sure nobody else's name or basket ever appears.

PreviousRedirects: 301, 302 and the chains betweenNextWhat time to first byte actually measures

More from Host Talk

Host Talk

How automatic certificate renewal works

Automatic renewal sounds like magic, but the mechanism is simple: a program proves to a certificate authority...

Host Talk

The TLS handshake in slow motion

Before a browser sends your password to a website, the two sides have to agree on how to scramble the...

Host Talk

What a control panel actually does

A hosting control panel is a web interface for tasks that would otherwise need a terminal. Behind the buttons...