Complete Guide to Debugging Cookie Issues

You flip the dark mode toggle. Works on one page, but resets on another. You embed a site on a different domain within an <iframe>
, but your settings vanish. Welcome to the world of browser cookies - a world where things can go wrong in many many different ways.
In this post, we’ll be looking at some of the most common ways cookies don’t work the way you’d expect them to.
Specifically we’ll look at cases where you try to set a cookie, but it doesn’t seem to get persisted, and then we’ll look at cases where your persisted cookie isn’t being included in your requests.
Let’s get started!
The server tries to set a cookie, but it doesn’t persist
Let’s start off with an easy one to get a sense for how to debug these types of issues.
Example: Your cookie is larger than 4KB
Cookies have a maximum size to them enforced by most browsers (and some web frameworks / reverse proxies). Cookies that are larger than 4KB are rejected with messages like:
- 400 Bad Request: Request Header or Cookie Too Large
- Request Header Fields Too Large
- Request Header Section Too Large
- The size of the request headers is too long
All pretty clear messages. We can test this directly by making a simple Express route, that sets a very long cookie:
app.get("/", (req, res) => {
const longCookie = "a".repeat(5000);
res.cookie("testLongCookie", longCookie, {}).send("Hello!");
});
When we go to that route, we can see that our Hello!
message displays, but no cookies were set:

This seems like a pretty obvious bug because the code is 4 lines - but in practice these bugs are usually a bit more subtle.
To debug this, we want to check the request on the Network tab in Chrome’s Dev Tools:

In the Response Headers section, you’ll find our attempt at setting a cookie via the Set-Cookie
header. You can also see a very clear error message:

This attempt to set a cookie via a Set-Cookie header was blocked because the cookie was too large. The combined size of the name and value must be less than or equal to 4096 characters.
For completeness, you can also check the Cookies tab, and if you hover over the problematic cookie, you’ll get the same error message:

Debugging these types of cookie issues
The same process we went through in the previous example will work for pretty much all the cookie issues where you tried to set it, but it doesn’t persist.
To debug these issues generally, we…
- Look for the request that should have set the cookie in the Network tab in Chrome
- Check either the Cookies tab or the
Set-Cookie
response header - Hover over it for a detailed error message
Easy enough, let’s apply this to some more problems:
Example: Your cookie is being set on the wrong domain
In addition to having a key and a value, cookies also have a bunch of different configuration points. One of these options is the domain
attribute, which dictates which server is allowed to receive the cookie.
Let’s update our code to set the domain in production, but avoid setting it in development:
// This is just an example, there are other options that you'd want to set
// like secure, httpOnly, etc, and you might not want to set domain at all
const getCookieOptions = () => {
if (process.env.NODE_ENV === "development") {
return {};
} else {
return {domain: "example.com"};
}
};
app.get("/", (req, res) => {
res.cookie("testCookie", "it-works!", getCookieOptions())
.send("Hello!");
});
As long as our response comes from example.com, when we aren’t in the development
environment, everything here works great.
Let’s say we deploy our product and set up preview deployments. Unlike our production environment, these preview deployments aren’t hosted on example.com. They instead get a unique URL per PR hosted elsewhere: https://pr_add_feature.hostingprovider.com
In our preview deployments, this cookie will never be set.
We can follow our same process as last time to see what’s wrong:

This attempt to set a cookie via a Set-Cookie header was blocked because it’s Domain attribute was invalid with regards to the current host url.
Put simply:
- Our server responded with a cookie that was made for example.com
- The server itself was hosted NOT on example.com but instead on hostingprovider.com
- The browser prevented that from happening
Quick aside: The Domain attribute can be odd
The behavior of the Domain attribute can feel a little counterintuitive. If you set a Domain attribute of example.com
, you are essentially specifying https://**.example.com
, because the Domain attribute applies to any subdomain of the specified domain as well.
If your server was responding from example.com, and you want the cookie to be sent ONLY for example.com, you shouldn’t specify a domain attribute at all.
You can read more about this here.
Other common reasons your server’s Set-Cookie fails
Here are a few more quick cases where the browser blocks a Set-Cookie attempt:
- Secure Flag Without HTTPS - If you set the
Secure
cookie attribute on a site served over HTTP (excluding localhost), the browser rejects the cookie. - SameSite Requires Secure - ****Setting SameSite=None demands that Secure also be set, or the browser will refuse the cookie. We'll look at SameSite in more depth later.
- Expired Immediately - A cookie with an expiration date in the past (or maxAge: 0) is deleted right away.
- This is pretty obviously expected behavior, but if you are debugging a cookie not being set, it’s worth the quick sanity check that the expiration is in fact in the future.
Example: HttpOnly cookies aren’t visible via document.cookie
One last honorable mention: it is a good practice to set the HttpOnly
attribute for any cookies that don’t need to be accessed via JavaScript and especially any sensitive cookies.
But for some non-sensitive cookies, you might want to set them via JS (e.g. a dark-mode toggle). Let’s adjust our route for a second:
app.get("/", (req, res) => {
res.cookie("canJsSeeMe", "yes").send("Hello!");
});
And then note that this cookie will be returned via document.cookie
:

But if we set HttpOnly:
app.get("/", (req, res) => {
res.cookie("canJsSeeMe", "yes", { httpOnly: true }).send("Hello!");
});
then document.cookie
returns nothing:

Also note that this works in both directions, this code, for example, does not error but also does not set a cookie:
// Doesn't work, HttpOnly can't be set for cookies set via JS
document.cookie="a=b;HttpOnly"
Now that we’ve gotten a good handle on when cookies have failed to be set in the browser, let’s switch gears to a different class of cookie problems.
There is a cookie set, but it isn’t being sent to our backend
We’ll start again with an easy example to get a better understanding of how to debug these types of errors.
Example: The cookie wasn’t sent because the paths don’t match
Let’s start by making our example app a little more complicated:
app.use(cookieParser());
app.get("/docs", (req, res) => {
if ("visitedDocs" in req.cookies) {
res.send("Welcome back to the docs!");
} else {
res.cookie("visitedDocs", "true", { path: "/docs", maxAge: ONE_YEAR });
res.send("Welcome to the docs for the first time!");
}
});
app.get("/blog", (req, res) => {
if ("visitedBlog" in req.cookies) {
res.send("Welcome back to the blog!");
} else {
res.cookie("visitedBlog", "true", { path: "/blog", maxAge: ONE_YEAR });
res.send("Welcome to the blog for the first time!");
}
});
We now have two routes, one for the docs and one for the blog. The first time you visit either, it will welcome you and set a cookie indicating you visited it. Every subsequent time, it will remember that you visited before.
You can bounce back and forth between them and see this works perfectly.
What happens when we update our /blog
route to look like this?
app.get("/blog", (req, res) => {
const messages = [];
if (!("visitedDocs" in req.cookies)) {
messages.push(
"It seems you haven't visited the docs yet. You can check them out <a href='/docs'>here</a>.",
);
}
if ("visitedBlog" in req.cookies) {
messages.push("Welcome back to the blog!");
} else {
res.cookie("visitedBlog", "true", { path: "/blog", maxAge: ONE_YEAR });
messages.push("Welcome to the blog for the first time!");
}
res.send(messages.join("<br>"));
});
We want to give our users a light nudge when they visit the blog that they should also check out our docs, and we do that by checking the visitedDocs
cookie in our /blog
route.
The problem? When a user visits /blog
they will always get the message that they haven’t visited the docs yet, even when they have. Even stranger, /docs
will correctly display whether or not they visited the docs already.
To debug this, let’s go back to the Network tab of our Dev Console and look at the request. The Request Headers section shows the problem, but not the cause:

The only cookie being sent is visitedBlog=true
. The Cookies tab is equally unhelpful at first:

…until you see the option “show filtered out request cookies.” Toggle that on and now we can see the problem clearly:

This cookie was blocked because its path was not an exact match for or a superdirectory of the request url’s path.
In other words, we set the Path
attribute for the visitedDocs
cookie to /docs
, so the browser will only send it to /docs
or paths underneath that like /docs/javascript
but will NOT send the cookie to any other path like /blog
.
To fix this, we can set the visitedDocs
's path to /
, which means it would be sent in requests to both /docs
and /blog
.
Debugging these types of cookie issues
Once again, the actual process for debugging these issues is pretty straightforward. We…
- Go to the request in the Network tab of the developer console.
- Look at the Cookies tab, making sure to turn on “show filtered out request cookies”
- Our problematic cookies will be highlighted and hovering over them will tell us why the browser didn’t send them in the request.
Let’s look at some more advanced versions of this!
Cross-Site requests and the inevitable cookie issues
Whenever you make a request from https://example.com to https://otherplace.com (in the browser, that is), you can run into issues where cookies don’t send when you think they should.
Aside: Why are Cross-Site requests more protective of cookies?
One straightforward reason is browsers want to be cautious of including a sensitive cookie when a user visits https://phishing-site.com and it tries to make a request to https://your-bank.com/transfer-funds.
However, the bank can also use CORS to protect against that specifically. CORS actually gives you the option to select which external domains you do allow requests from, and those might be completely reasonable use cases for third-party cookies.
The problem is those types of cookies can also be used to track you across the internet.
Let’s imagine https://yourfavoritesocialmediasite.com was able to set a cookie with a unique ID for you. There’s nothing inherently wrong with that - pretty much every site does a version of that for authentication.
But now let’s imagine that sites like https://momandpopstore.com or https://sensitivehealthtopics.com include advertisements that load from https://yourfavoritesocialmediasite.com. If the browser includes that cookie that uniquely identifies you, your favorite social media site is effectively able to see all the different websites you visit.
CORS won’t protect you here because https://yourfavoritesocialmediasite.com WANTS the cookie to be sent to them, whereas https://your-bank.com definitely doesn’t.
All that to say - if you are doing anything with cookies that need to work across domains, it’s possible, but you will be fighting against user’s (very reasonable) privacy protections, and it’s worth considering if you have other options.
Example: Cookies not sending on initial page load (a.k.a. what is SameSite)
Let’s get back to debugging cookie problems and look at another common one. We’ll update our example app one more time:
app.get("/", (req, res) => {
res.cookie("check_me", "ok", { sameSite: "strict", maxAge: ONE_YEAR });
if ("check_me" in req.cookies) {
res.send("check_me was sent");
} else {
res.send("check_me was NOT sent");
}
});
This seems pretty straightforward. The only notable difference is we are setting SameSite
to Strict
. SameSite
is a cookie attribute that lets you control when cookies are sent in cross-site requests, and strict is… the strictest setting.
The first time we go to our page we get the check_me was NOT sent
message and every subsequent time we refresh we get check_me was sent
.
Down the road, we start getting reports that users are randomly seeing check_me was NOT sent
when they first go to the page. When you narrow it down, you find that it occurs when an external site links to your site.
How do we debug this? Same way we did the rest of them, we go to the request in the Network section, check the cookies tab, and see:

This cookie was blocked because it had the “SameSite=Strict” attribute and the request was made from a different site. This includes top-level navigation requests initiated by other sites.
Put differently, the user navigated from https://othersite.com to https://yoursite.com, so the browser will not send any Strict cookies initially.
In some cases, this is a nice, protective feature. In others, it’s an annoying bug and you’ll want to switch to Lax.
For a brief overview of the different SameSite settings:
Strict
: Cookies only get sent on same-site requests. In other words, if you follow a link fromexample.com
toexample.com/page2
, that’s same-site, so the cookie will be sent. But if you submit a form fromanother-site.com
toexample.com
, that’s cross-site and cookies won’t be included.Lax
: Cookies are sent along with top-level navigations (like clicking a link fromanother-site.com
toexample.com
) but are not sent on other cross-site requests, such as iframes or programmatic requests (e.g.fetch
, Ajax, or form submissions from a different domain).None
: The cookie can be sent on all cross-site requests. However, settingSameSite=None
requiresSecure
to be set (your site must be served over HTTPS). WithoutSecure
, browsers will reject the cookie.
More cross-domain issues: Don’t forget includeCredentials
We’ve primarily looked at navigation events. A user loaded /docs
. A user was redirected to example.com
.
Another common way that cookies are sent and received is via fetch
calls, or similar requests made from JavaScript.
By default, a cross-site fetch
request doesn’t include cookies unless you explicitly allow it:
// By default, cookies are NOT included in cross-site requests
fetch("<https://othersite.com/api>", {
method: "GET",
});
// To include cookies, you need to add:
fetch("<https://othersite.com/api>", {
method: "GET",
credentials: "include",
});
One important thing to note here, is that these requests will still respect all the other settings we talked about. Even when you do add credentials: "include"
, your cookies still may not send for SameSite reasons or because the user’s browser blocks all third-party cookies.
Summary
Cookies can both be incredibly easy and a giant pain to work with.
When you are debugging an issue related to cookies, a good default is to always look at the request in your DevTools console.
If the cookie isn’t being persisted when it should be, you should see an error message on the Set-Cookie
header.
If the cookie isn’t being sent when it should be, you should see an error message on the Cookies tab, after turning on “show filtered out request cookies”
There are cases where you will get silent failures (like trying to set an HttpOnly cookie from JavaScript) and you’ll need to examine each of those cases individually. Look at the attributes you are setting and see if any of them dictate a requirement that you aren’t meeting. Additionally, if nothing stands out there, double check the default attributes of the cookie being set.
Finally, third-party cookies are a common source of headaches because your legitimate use case could almost certainly be used to violate other people’s privacy (if used by the wrong people). Whenever possible, try to avoid requiring them.
Hopefully you now have the tools to tackle any cookie issue that comes your way!