They both hit “your machine,” but they don’t behave the same. Here’s what changes with DNS, IPv6, cookies, CORS, TLS, proxies, Docker, and OAuth.
Introduction: “It works on 127.0.0.1 but not on localhost”
You’ve probably said (or heard): “It works when I use 127.0.0.1
, but it breaks with localhost
” Same computer, same port, so why the drama?
Short answer:
127.0.0.1
It is an IPv4 address in the loopback block. No name lookup involved.localhost
It is a hostname that your OS resolves (usually via the hosts file) to a loopback address. On many systems, that’s::1
(IPv6) first, then127.0.0.1
(IPv4).
Those tiny differences cascade into real-world effects (certs, cookies, CORS, proxies, Docker, OAuth…). Let’s break it down like a teammate would with examples, gotchas, and copy-paste fixes.
TL;DR: Quick Cheatsheet
- Type:
127.0.0.1
= numeric IPv4 address;localhost
= name that resolves to loopback. - Resolution:
127.0.0.1
skips DNS;localhost
resolves via the hosts file (and is a special-use name). - IPv6:
localhost
Often resolves to::1
first. If your server isn’t listening on IPv6,localhost
may fail while127.0.0.1
works. - Origins:
http://localhost:3000
andhttp://127.0.0.1:3000
are different origins ⇒ CORS & cookies differ. - TLS: A cert for
localhost
does not cover127.0.0.1
(and vice versa) unless SANs include both. - Proxies: Add both
localhost,127.0.0.1
toNO_PROXY
. - Docker: Inside containers, both point to the container, not your host. Use
host.docker.internal
(Mac/Win) to reach the host. - OAuth: Callback URIs must match exactly.
localhost
≠127.0.0.1
.
1) What “localhost” actually is (and isn’t)
localhost
is a special-use hostname. OSes map it locally (no real DNS). You’ll see it in your hosts file:- macOS/Linux:
/etc/hosts
- Windows:
C:\Windows\System32\drivers\etc\hosts
Typical lines:
127.0.0.1 localhost
::1 localhost
- Don’t repoint
localhost
to anything else. Standards and lots of software assume it’s loopback. - 127.0.0.1 is one of many loopback IPv4 addresses. The entire
127.0.0.0/8
block loops back. Convention picks.1
.
2) IPv6 surprise: localhost
may resolve to ::1
first
On many systems, localhost
prefers IPv6 ::1
. If your dev server only binds to IPv4, localhost
can fail while 127.0.0.1
works.
Repro scenario (Node/Express)
const express = require("express");
const app = express();
app.get("/", (req, res) => res.send("hi"));
app.listen(3000, "127.0.0.1", () => {
console.log("Listening on IPv4 only: http://127.0.0.1:3000");
});
- Visiting
http://127.0.0.1:3000
→ ✅ - Visiting
http://localhost:3000
→ ❌ on machines wherelocalhost
→::1
(server isn’t listening on IPv6).
Fixes
- Bind to both families by listening on the unspecified address:
- IPv6:
"::"
(covers IPv6 and, on most stacks, IPv4-mapped) - IPv4:
"0.0.0.0"
(IPv4 only)
app.listen(3000, "::", () => {
console.log("Listening on IPv6 unspecified (often also covers IPv4)");
});
Or run two servers (one per family) if your stack needs it.
3) Same machine, different origin: CORS & cookies
The browser origin is scheme + host + port. That means:
http://localhost:3000
≠http://127.0.0.1:3000
- To the browser, they are two different origins.
CORS example
If your API runs on http://127.0.0.1:8080
and your frontend runs on http://localhost:5173
, CORS must explicitly allow the exact origin:
// Example with Express + cors
const cors = require("cors");
app.use(cors({
origin: ["http://localhost:5173", "http://127.0.0.1:5173"], // include both if needed
credentials: true
}));
Cookies example
Cookies are also host-scoped. A cookie set on localhost
won’t automatically be sent to127.0.0.1
, and vice versa.
- You can set host-only cookies on
localhost
or on an IP literal, but theDomain=
attribute can’t be an IP (browsers ignore it). Just omitDomain
for local dev; the cookie will be host-only.
Pro tip: Pick one host (either localhost
one 127.0.0.1
) for your full local stack to avoid silent cookie/CORS mismatches.
4) TLS & dev certificates: “Why does my cert fail on 127.0.0.1?”
A certificate for localhost
doesn’t validate 127.0.0.1
unless the cert’s Subject Alternative Names (SANs) include both the hostname and the IP.
Solutions
- Use a dev CA like mkcert to generate a cert with SANs for:
DNS:localhost
IP:127.0.0.1
- (optionally)
IP:0:0:0:0:0:0:0:1
(that’s::1
)
mkcert localhost 127.0.0.1 ::1
- Or just stick to
https://localhost
consistently during development.
5) Proxies, corporate networks, and NO_PROXY
Many environments route HTTP(S) via a proxy, except for local addresses. To make sure your tooling really bypasses the proxy, set the NO_PROXY
environment variable to both:
# bash/zsh
export NO_PROXY=localhost,127.0.0.1
On Windows (PowerShell):
$env:NO_PROXY="localhost,127.0.0.1"
Also, tick “Bypass proxy for local addresses” in OS/browser proxy settings if available.
6) OAuth & redirect URIs: exact matches only
OAuth providers match redirect URIs exactly. This will bite you:
- Registered:
http://localhost:3000/callback
- Actual:
http://127.0.0.1:3000/callback
→ ❌ mismatch
Pick one (I recommend localhost
) and register exactly that. Some providers also support loopback-only rules (e.g., any port on http://127.0.0.1
), but don’t assume, check the provider.
7) Docker, containers, and VM gotchas
- Inside a container,
localhost
/127.0.0.1
Refer to the container itself, not the host. - To access the host from a container:
- Docker Desktop (Mac/Windows):
host.docker.internal
- Linux: set up a bridge alias or use the host network for dev.
Port mapping reminder: If you run docker run -p 3000:3000 …
, the service inside the container must bind to 0.0.0.0
(or ::
) inside the container, otherwise the port won’t be reachable from the host.
# inside container, do NOT bind only to 127.0.0.1
app.listen(3000, "0.0.0.0")
8) Hosts file, resolution order, and Debian’s 127.0.1.1 quirk
- Some Linux distros (Debian/Ubuntu) add a line like:
127.0.1.1 my-hostname
- That’s not
localhost
; it maps your hostname to a loopback address (used by some tools). Keeplocalhost
on127.0.0.1
and::1
. - If you accidentally remove
::1 localhost
,localhost
May no longer resolve to IPv6, changing behavior in apps that prefer IPv6. - If you break the hosts file (e.g., mapping
localhost
to an external IP), prepare for weird bugs: timeouts, proxy bypass failures, and tooling that assumes loopback.
9) Browser security: secure contexts, HSTS, and service workers
- Browsers treat loopback (both
localhost
and127.0.0.1
) as “potentially trustworthy” even over HTTP. That’s why you can use features like Service Workers during local dev without HTTPS. - If you accidentally set HSTS on
localhost
, the browser might force HTTPS thereafter. Clear site data or HSTS settings if that happens.
Practical tip: Prefer localhost
dev URLs if you routinely use progressive web APIs; it’s the path of least resistance.
10) Performance differences? Basically none.
Resolution of localhost
is a trivial local lookup. 127.0.0.1
It is a numeric literal. Either way, you’re going through the loopback interface; the difference isn’t measurable for real work. If something is slow, it’s your app, not the loopback choice.
11) When to use which: pragmatic guidelines
Use localhost
when you want:
- Simpler dev URLs and certificates.
- Progressive web APIs and service workers without fuss.
- Consistency across teams/tools (most docs use
localhost
).
Use 127.0.0.1
when you want:
- To force IPv4 (avoid
::1
surprises). - To bypass any misconfigured hosts/DNS shenanigans.
- To test behavior with IP literals (e.g., cookie or CORS edge cases).
Team policy tip: Pick one for your whole local stack (front-end, API, auth callback) and stick to it.
12) Troubleshooting checklist (copy-paste friendly)
- What does
localhost
resolve to?
# macOS/Linux
getent hosts localhost || cat /etc/hosts | grep localhost
# Windows (PowerShell)
Resolve-DnsName localhost
2. Is your server bound to the right family?
# If you used 127.0.0.1, you’re IPv4-only.
# If localhost resolves to ::1, bind to "::" or also bind to IPv6.
3. Are origins consistent?
Use all localhost
or all 127.0.0.1
. Fix the CORS origin
list accordingly.
4. Cookies not sticking?
Host-only cookies don’t cross hosts. Don’t set Domain=
to an IP. Keep one host.
5. TLS failing?
Regenerate the dev cert with SANs for both localhost
and 127.0.0.1
(and ::1
).
6. Proxy weirdness?
Ensure NO_PROXY=localhost,127.0.0.1
.
7. OAuth redirect mismatch?
Update the provider config to match the exact host and port you use locally.
8. Docker access failing?
Remember: inside the container localhost
is the container. Use host.docker.internal
(Mac/Win) or proper host networking.
Real-world mini-scenarios
A) React + API CORS flake
- Symptom: “CORS error” in dev console.
- Root cause: App at
http://localhost:5173
, API athttp://127.0.0.1:8080
, CORSorigin
Only allowed one host. - Fix: Standardize hosts or list both in CORS.
B) Cookie-based session “sometimes logged out”
- Symptom: Logging in works on
localhost
tab, but the127.0.0.1
tab isn’t authenticated. - Root cause: Host-only cookie.
- Fix: Use a single host across app+API during dev.
C) “Site works on 127.0.0.1 but not on localhost”
- Symptom: Connection refused on
localhost
. - Root cause: App bound only to IPv4;
localhost
resolved to::1
. - Fix: Bind to
::
or also bind to IPv6.
D) OAuth callback mismatch
- Symptom: OAuth provider rejects redirect URI.
- Root cause: Registered
http://localhost:3000/callback
, app usedhttp://127.0.0.1:3000/callback
. - Fix: Update the provider or your app to match exactly.
Conclusion
“Localhost vs 127.0.0.1
” sounds academic until it breaks your cookies, CORS, TLS, or OAuth. The differences are small but practical:
localhost
is a name (often IPv6::1
first).127.0.0.1
It is an IPv4 address (no name lookup).- Browsers treat them as different origins.
- Certs, proxies, Docker, and auth flows all notice the difference.
Best practice: pick one host for your entire local stack, bind your servers to the right address family, and include both in your dev tooling where it makes sense.
Call to Action
What’s the weirdest localhost vs 127.0.0.1
bug you’ve hit? Drop it in the comments, let’s save someone else a headache.
👉 Share this with a teammate debugging CORS or cookies.
🔖 Bookmark this for your next dev environment setup.
Pro Tip (bonus)
Create a .env.development
that standardizes your local host:
# Pick one and stick to it
VITE_APP_ORIGIN=http://localhost:5173
API_ORIGIN=http://localhost:8080
# or, if you must force IPv4:
# VITE_APP_ORIGIN=http://127.0.0.1:5173
# API_ORIGIN=http://127.0.0.1:8080
…and wire your app to read from it. One switch, fewer surprises.
Leave a Reply