A user finishes login, the application writes a session cookie, and the next request shows up anonymous. The ticket names the load balancer, because that was the component added the same week. The header may have been removed or rewritten on the way to the browser, or it may have arrived intact at a node that has no session for it.
Trace the login response
Start in the application, on the response that creates the session, before that response leaves the process. Log the Set-Cookie names there. If the session cookie is missing at that point, the application omitted it, and the load balancer is the wrong place to edit.
After TLS termination moves to the load balancer, the application often still classifies the request as HTTP. It learns the browser's scheme from a forwarded header, usually X-Forwarded-Proto, and only from a proxy whose address you have trusted. If any caller may set that header, a client can spoof the scheme. If no address is trusted, every request looks like cleartext, and the cookie policy follows that classification.
A policy that adds Secure only when the request looks like HTTPS will emit the session cookie without that flag. For a first-party cookie with SameSite=Lax, the browser still stores it, because the browser's own connection to the load balancer is HTTPS. SameSite=None is stricter. A cross-site POST from an identity provider on another registrable domain needs None, and current Chrome discards SameSite=None unless Secure is present. The application sent the header and the load balancer forwarded it, and the browser threw it away. That is the sequence filed as the load balancer dropping the cookie.
The same wrong scheme also corrupts redirect targets when the cookie flag is fixed in policy and the redirect URL still follows the internal scheme. The application sets the flag from policy, then builds Location from the scheme it sees and sends the browser to http://. The load balancer upgrades that hop to HTTPS. The browser withholds a Secure cookie from the HTTP request in the middle. Middleware on that hop that treats a missing cookie as an anonymous user will redirect to login, and the browser loops. If the hop is only a redirect, the user often reaches the next HTTPS URL with the cookie still stored, and the gap stays out of the logs. On ASP.NET Core the correction is forwarded-headers middleware limited to the load balancer's addresses. Django reads the same fact from SECURE_PROXY_SSL_HEADER. Run the check through the load balancer: the public URL is HTTPS, the upstream connection is HTTP, and the login response carries the session cookie with the attributes you intended. A test that injects the header inside the process still passes while the trust list is empty.
Read every Set-Cookie on that response. Login often sets a session cookie and a CSRF cookie, and the load balancer may add an affinity cookie on the way out. nginx strips Set-Cookie when proxy_hide_header names it, a line that sometimes survives from an earlier upstream whose cookie you did not want on the public host. A blanket hide removes every instance. I have also seen an API gateway store response headers in a map keyed by name, so the second Set-Cookie overwrote the first and only the affinity cookie remained. Health checks kept passing while users came back anonymous.
Header limits get filed on the same ticket, and the load balancer rejects the request. An Application Load Balancer allows 16 KB for a single header, 64 KB for the whole request header block, and 32 KB for the whole response header block. Past the request limits it answers 400 or 413. Past the response limit it answers 502. nginx, with the default large_client_header_buffers, rejects a Cookie line longer than one buffer and answers 400. You see that error first on an account whose session cookie embeds a permission list or a token. Browsers commonly cap one cookie at 4096 bytes, the minimum RFC 6265 tells them to support, so an oversized session cookie fails in the jar even when the load balancer would have forwarded it. Read the cookie body before raising a buffer. An opaque session id fits, and a serialized profile hits the same limit the next time a claim is added.
Read the attributes the browser stored
If the application emitted the cookie and a capture on the public side of the load balancer still shows it, the browser can still refuse to keep it, based on the attributes alone.
Domain and Path decide whether the cookie is attached to the next request. A host-only cookie, with no Domain attribute, is stored for the host the browser called, the public name. A different upstream hostname leaves that in place, and a rewrite replaces the host the browser will accept. nginx changes the attributes with proxy_cookie_domain and proxy_cookie_path, and a block copied from an internal environment will stamp Domain=app-7.internal, or a path that existed only on the upstream. The browser rejects a Domain that fails to match the response host, so the cookie never enters storage. If the rewritten Domain matches some other host you own, the cookie is stored there and is never sent to the public site. The application log shows no Cookie header and no error.
One login shows the rewrite cleanly. The application emits Set-Cookie: sid=8f3c; Path=/; Secure; HttpOnly; SameSite=Lax. After nginx, the public response reads Set-Cookie: sid=8f3c; Domain=app-7.internal; Path=/; Secure; HttpOnly; SameSite=Lax. The session id is intact. The Domain is why the browser will not return the cookie to the name in the address bar. Compare those two lines before changing stickiness, timeouts, or the session store.
A cookie whose name starts with __Host- must be Secure, must use Path=/, and must omit Domain. Adding Domain, or changing Path, makes the browser discard it. That prefix is what keeps a proxy from widening the cookie. A load balancer rule that rewrites attributes on every response removes the constraint.
SameSite=Lax is a sound default for a first-party session, and the browser withholds it on a cross-site POST. It shows up when an identity provider posts the login completion from another site, or when a magic link opens in a context the browser classifies as cross-site. Those requests need SameSite=None together with Secure. Move the identity provider onto a different registrable domain and a cookie the old arrangement allowed starts getting discarded. On the failing login, compare SameSite and Secure on the Set-Cookie the browser received with the Cookie header on the next request. One of those attributes usually accounts for the failure, and it was accepted until the public host or the scheme changed.
Affinity cookies sit in the same jar and get read as if they were the session. AWSALB, AWSALBCORS, ARRAffinity, and a HAProxy cookie that names a server tell the load balancer which node should see the next request. If the application session lives in memory on that node, the affinity cookie is the only reason the next request finds it. Drain the node, or replace it in a deploy, and the affinity cookie points at a target that is gone. The load balancer sends the request to a live node, the session cookie arrives, and nothing in that node's memory matches the id. Another stickiness rule repeats the logout on the next deploy, because the new node has no copy of the session.
Patch the hop that failed
Correct the forwarded scheme or the Secure policy when the application's own login response is missing the session cookie. Leave the previous login path able to honor a session it already issued for the length of the rollout, and pair that with a switch that sends new logins back to the previous path without a deploy. A bad Secure flag or a bad redirect hits everyone who signs in, and those users need a cookie the old path can still read while the new path is corrected.
Disagreement between the upstream Set-Cookie and the public one means a hide or a rewrite is on the path. Remove a blanket proxy_hide_header Set-Cookie, or set proxy_cookie_domain to the public suffix and leave a one-line note in the file naming that suffix. After the change, compare the upstream header with the public header on one login. They may differ by that suffix alone.
The session cookie can also arrive at a node that has nothing stored under that id. Put the session in a store every node reads, and leave the cookie as an id. Sticky routing can cover a short period of reading an old store and a new one, if the day you turn stickiness off is already chosen. Without that day, requests that miss the store stay out of the error rate until a node is replaced, and the logout shows up with no deploy to bisect.