DEV BOX

'26/11/02 UPDATE

WEB / COOKIES / FIELD NOTES

Do cookies stay separate when two apps run on different localhost ports?

If two apps that use the same cookie name run on localhost:4371 and localhost:4372, logging in to one overwrites the other's cookie and logging out of one ends both. Cookies are stored per host name and ignore the port. Using server logs from two Node.js servers opened in a Chromium 152-based browser, this note shows the behaviour, why SameSite=Strict and __Host- do not split ports either, and how to choose between separate cookie names and separate host names.

If you run two apps, one on localhost:4371 and one on localhost:4372, and logging in to one ends the login on the other, the cause is that cookies do not care about ports. The browser treats the two addresses as different origins, but it ignores the port when it stores and sends cookies. After reading this, you will be able to confirm the behaviour yourself from the Cookie header in a server log, and decide whether to separate two apps by cookie name or by host name.

The experiment was run on 2026-10-27 on macOS 27.0.1. Two servers built with node:http in Node.js v24.21.0 listened on ports 4371 and 4372 (on 127.0.0.1 and ::1 only) and were opened in the built-in browser of the Claude desktop app (Chromium 152.0.7977.167). For each request, each server logs the Host header and the lab's own cookies from the Cookie header (names starting with lab1130_ or __Host-lab1130_), and each page reports its own localStorage value back to the server so that it lands in the same log. The cookie name lab1130_sid and its values exist only for the experiment. Google Chrome itself, Safari and Firefox were not tested, and no HTTPS address was tried.

A cookie from 4371 goes to 4372

First, localhost:4371/login was opened. The server sent the header below, with no Domain attribute.

Set-Cookie: lab1130_sid=app-4371; Path=/; Max-Age=600; HttpOnly; SameSite=Lax

Next, localhost:4372/ was opened, and the 4372 server logged Cookie: lab1130_sid=app-4371. The 4372 app never created that cookie. The same page's localStorage, on the other hand, was empty, and the value 4372 stored there did not show up when 4371 was opened again. The browser does treat the two addresses as different origins. Only the cookie followed a different rule.

Server log. 4371's /login sent Set-Cookie: lab1130_sid=app-4371; Path=/; Max-Age=600; HttpOnly; SameSite=Lax. The following GET localhost:4372/ carried Cookie: lab1130_sid=app-4371, and the 4372 page reported localStorage before=(none) now=set-by-4372. The next GET localhost:4371/ carried the same cookie, and its localStorage was before=set-by-4371 now=set-by-4371.
Server log after logging in on localhost:4371 and then opening localhost:4372. The cookie crossed to 4372; localStorage stayed separate per port.

The port is not part of how cookies are matched

An origin is defined by three things: scheme, host and port. Web Storage and IndexedDB are kept separately for each origin, which is why localStorage was split by port. Cookies define origins differently.

Under RFC 6265 (2011), which defines how cookies work, a cookie received without a Domain attribute becomes a host-only cookie. Its domain field holds the host name of the address the request was sent to, without the port. The conditions checked when a cookie is attached to a request (host name, path, whether it is Secure) do not include the port either. So a cookie received from localhost:4371 becomes a cookie of the host localhost, and it goes to any port on that host.

RFC 6265 itself treats this as a weakness. Its introduction already states that the cookies of a host are shared by all the ports on that host, and section 8.5 of its security considerations begins with "Cookies do not provide isolation by port." The same section recommends (SHOULD NOT) against running mutually distrusting services on different ports of the same host while keeping sensitive information in cookies. The revision that will replace RFC 6265 (rfc6265bis) is, as of 2026-10-27, still an Internet-Draft (revision 22) without an RFC number, and it keeps these sentences as they are.

Same name: one overwrites the other, and one logout ends both

Next, the experiment logged in on 4372. When 4372 sent lab1130_sid=app-4372 under the same name, the existing cookie with the same name, domain and path was removed and replaced by the new one. From then on, requests to 4371 carried app-4372. In apps that store sessions separately on each server, 4371 receives a session ID it never issued, and if it cannot find that ID the user looks logged out. In the other direction, logging out on 4371 deleted the cookie with Max-Age=0, and the next request to 4372 had no cookie either.

This is what happens when two apps keep the same session middleware's default cookie name and run side by side. For example, in express-session, the session middleware for Express, the default cookie name is connect.sid. The package's README says that apps running on the same host name need a different name each, and adds that a different scheme or port does not make a different host name. This experiment did not use express-session; it recreated the same situation with node:http.

So the experiment was repeated with the cookie names lab1130_sid_4371 and lab1130_sid_4372. The overwriting and the shared logout went away. But requests to 4371 carried the cookies of both apps. The names no longer collide, yet each app still receives the other app's session cookie.

Server log. The /login request to 4372 carried Cookie: lab1130_sid=app-4371, and 4372 sent Set-Cookie: lab1130_sid=app-4372. The next GET localhost:4371/ carried Cookie: lab1130_sid=app-4372. After 4371's /logout sent lab1130_sid=; Max-Age=0, GET localhost:4372/ had Cookie: (none). With one name per app, GET localhost:4371/ carried Cookie: lab1130_sid_4371=app-4371; lab1130_sid_4372=app-4372, and after 4371's /logout-named, GET localhost:4372/ still had Cookie: lab1130_sid_4372=app-4372.
With the same name (lab1130_sid), the two apps overwrote each other's login and logged out together. One name per app removes that, but both cookies still reach both apps.

SameSite and __Host- do not split ports either

Would stricter cookie attributes keep them apart? 4371 sent a cookie with the __Host- prefix, Secure and SameSite=Strict.

Set-Cookie: __Host-lab1130_sid=app-4371; Path=/; Max-Age=600; Secure; HttpOnly; SameSite=Strict

A __Host- cookie is stored only when it has Secure and Path=/ and no Domain attribute, which ties it to one host. The address was http, but this browser did not apply the https requirement to a cookie set by localhost, so the cookie was stored. And this cookie was also sent to 4372. The port is the one part of the origin that even __Host- cookies still ignore (rfc6265bis, revision 22).

SameSite=Strict sends a cookie only with requests that start from the same site as the one that set it. But the HTML Standard decides "same site" by comparing only the scheme and the registrable domain (or the host, when there is none), and it ignores the port. A fetch('http://localhost:4371/probe', {mode: 'no-cors', credentials: 'include'}) from a localhost:4372 page carried the Strict cookie, and the request's Sec-Fetch-Site header was same-site. The same request from a 127.0.0.1:4372 page was cross-site and carried no cookie. The conditions under which fetch sends cookies to another origin are covered in Why a CORS error can mean the server never saw the POST.

Server log. 4371's /login-host sent Set-Cookie: __Host-lab1130_sid=app-4371; Path=/; Max-Age=600; Secure; HttpOnly; SameSite=Strict. GET localhost:4372/ carried Cookie: __Host-lab1130_sid=app-4371. fetch('http://localhost:4371/probe', {mode: 'no-cors', credentials: 'include'}) from the localhost:4372 page carried the same cookie with Sec-Fetch-Site: same-site. The same fetch from a 127.0.0.1:4372 page had Cookie: (none) and Sec-Fetch-Site: cross-site.
Even with the __Host- prefix and SameSite=Strict, the cookie went to the other port. The fetch from the 4372 page to 4371 differed only by port, so it was judged same-site and carried the cookie.

Separate host names keep cookies apart

Since cookies follow the host name, give each app its own host name. After logging in on a.localhost:4371, requests to b.localhost:4372 and localhost:4372 carried no cookie. a.localhost:4372, which differs only by port, did get the cookie. localhost and 127.0.0.1 are different hosts too, so no cookie passed between them. A cookie received from 127.0.0.1:4371, however, went to 127.0.0.1:4372. Opening apps by IP address does not split cookies by port either.

localhost and the names under it may be assumed to resolve to the loopback address (RFC 6761). This Chromium-based browser opened a.localhost and b.localhost without any entry in /etc/hosts. How Safari, Firefox and tools outside the browser resolve these names was not checked.

Server log. After a.localhost:4371/login sent Set-Cookie: lab1130_sid=app-4371, GET a.localhost:4372/ carried Cookie: lab1130_sid=app-4371, while GET b.localhost:4372/ and GET localhost:4372/ had Cookie: (none). After localhost:4371/login, GET 127.0.0.1:4372/ had Cookie: (none). After 127.0.0.1:4371/login, GET 127.0.0.1:4372/ carried Cookie: lab1130_sid=app-4371, and GET localhost:4372/ had Cookie: (none). The /logout lines between steps send Set-Cookie: lab1130_sid=; Path=/; Max-Age=0.
Separate host names keep cookies apart. a.localhost:4372 and 127.0.0.1:4372, which differ only by port, still got the cookie.

This behaviour may change. Chrome Platform Status has an entry called "Origin-Bound cookies (by default)" that would bind each cookie to the origin that set it, port included, and as of 2026-10-27 its status is Proposed. The results above come from a Chromium 152-based browser; a browser with that feature turned on may behave differently.

Choosing between a cookie name and a host name

DEV NOTES

Cookies split by host name, localStorage by origin

On the same localhost, a different port still means the same cookie jar

  • Cookies: per host name

    localhost:4372 Cookie: lab1130_sid=app-4371

    • A cookie from 4371 rode along to 4372
    • Same name: one overwrites the other, one logout ends both
  • localStorage: per origin

    localhost:4372 before=(none)

    • An origin is scheme, host and port
    • The 4372 page did not have 4371's value
  • Fix 1: one cookie name per app

    lab1130_sid_4371 · lab1130_sid_4372

    • No more overwriting or shared logout
    • Each app still receives the other app's cookie
  • Fix 2: separate host names

    b.localhost:4372 Cookie: (none)

    • a.localhost cookies did not go to b.localhost
    • localhost cookies did not go to 127.0.0.1

Run on 2026-10-27 in a Chromium 152.0.7977.167-based browser against two Node.js v24.21.0 servers. Cookies with SameSite=Strict and the __Host- prefix also crossed to the other port.

Give each app its own cookie name if you only need to stop the overwriting and shared logout; give each app its own host name if the apps must not receive each other's cookies.

If all you need is to stop one app's login from overwriting or ending the other's, give each app its own cookie name. If the two apps must not receive each other's cookies, give them separate host names, such as a.localhost and b.localhost; this works for cookies set without a Domain attribute. When in doubt, log Host and Cookie together on the server. A cookie follows the host name the request goes to, not the app that created it.

THE END
RSS feedBack to the library
COMMENTS BOX

Add your perspective.

Share a question, another approach, or something you have tried.

Newest first

Checking sign-in…

Loading comments…