localhost:5173 / localhost-vs-127

localhost:5173 vs 127.0.0.1:5173

By default the Vite dev server listens on exactly one loopback address — whichever one localhost resolves to first — and on current Node that is often ::1, so http://127.0.0.1:5173 is refused while http://localhost:5173 answers.

error
$ curl http://127.0.0.1:5173/
curl: (7) Failed to connect to 127.0.0.1 port 5173: Connection refused

Why does the dev server answer on only one of the two addresses?

The default for server.host is the string "localhost", not an address. Vite passes that name straight to Node’s httpServer.listen(port, host). Node resolves the name with dns.lookup, takes the first usable result and binds to that single address. localhost usually has two entries, ::1 and 127.0.0.1, and only one of them gets a listening socket.

bash — macOS, Node 24.19.0, default config
$ lsof -nP -iTCP:5173 -sTCP:LISTEN
node  94596  you  16u  IPv6  TCP [::1]:5173 (LISTEN)

$ curl -s -o /dev/null -w "%{http_code}\n" http://[::1]:5173/
200
$ curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:5173/
000   # connection refused

Which address wins is decided by Node, not by Vite

Since Node 17 the default order of dns.lookup is "verbatim": results come back in the order the operating system returns them. On macOS and many Linux setups the system lists ::1 before 127.0.0.1, so the dev server ends up on IPv6 loopback. Node before 17 sorted IPv4 first, which is why older answers say localhost:5173 means 127.0.0.1 — that was true for the runtime they were written on.

  • Node 17 and later: default order verbatim, the OS decides.
  • dns.setDefaultResultOrder() or --dns-result-order changes it; valid values are ipv4first, ipv6first and verbatim.
  • ipv6first exists since Node 22.1.0 and 20.13.0.

The Local line tells you which one Vite got

Vite compares two lookups of localhost: Node’s default one and a verbatim one. If they differ, it prints the address Node actually used instead of the name. A Local line reading http://127.0.0.1:5173/ is therefore not a bug — it means the lookup order was changed and the server is on IPv4. When the default order is already verbatim, the check is skipped and the line says localhost.

bash
$ NODE_OPTIONS=--dns-result-order=ipv4first npm run dev

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
# now 127.0.0.1 answers and [::1] is refused

Why the browser works while another tool gets ECONNREFUSED

A client that is given the name localhost can try both addresses and fall back to the one that answers — curl did exactly that in the test above and connected via ::1. A client that is given the literal 127.0.0.1 has nothing to fall back to. That is the typical split: the browser tab on localhost:5173 loads, while a test runner, a script, a container health check or a config with a hardcoded 127.0.0.1:5173 is refused by the same running server.

Pin the address instead of relying on the lookup

Setting server.host to 127.0.0.1 makes Vite bind IPv4 loopback only, and the Local line shows that address. It stays loopback, so nothing on the LAN can reach it. The reverse, host "::1", pins IPv6. Both are listed by Vite as loopback hosts, so they count as Local, not Network.

vite.config.js
export default {
  server: {
    host: '127.0.0.1'   // one fixed address, independent of DNS order
  }
}

What --host changes for this question

With --host or host: true, Vite passes no host at all, and Node listens on the unspecified address. In the local test that was a single IPv6 socket on *:5173 that answered on both 127.0.0.1 and ::1, because most operating systems let :: accept IPv4 as well. It solves the split, but it also exposes the server to the network — a wider change than the problem needs.

bash
$ npm run dev -- --host
  ➜  Local:   http://localhost:5173/
  ➜  Network: http://192.168.178.85:5173/   en0

$ lsof -nP -iTCP:5173 -sTCP:LISTEN
node  …  IPv6  TCP *:5173 (LISTEN)   # 127.0.0.1 and ::1 both 200

Both addresses pass the host and CORS checks

The refusal is a socket problem, never a rejection by Vite. server.allowedHosts lets localhost and every IP address through by default, and the default server.cors origin pattern accepts localhost, 127.0.0.1 and [::1] on any port. If 127.0.0.1:5173 connects but the page misbehaves, the cause is elsewhere: to the browser, localhost:5173 and 127.0.0.1:5173 are two different origins with separate cookies and storage.

# faq

Questions

Why does 127.0.0.1:5173 refuse while localhost:5173 works?

Because the default host is the name localhost and Node binds it to the first address it resolves to. On current Node that is often ::1, so nothing listens on 127.0.0.1.

How do I make the Vite dev server answer on 127.0.0.1?

Set server.host to "127.0.0.1" or start it with --host 127.0.0.1. It stays loopback-only and the Local line shows 127.0.0.1.

Why does the Local line say 127.0.0.1 instead of localhost?

Node’s lookup order was changed away from verbatim, for example with --dns-result-order=ipv4first, and resolved localhost to a different address than a verbatim lookup. Vite prints the address it actually used in that case.

Is ::1 the same as localhost?

::1 is the IPv6 loopback address, 127.0.0.1 the IPv4 one. localhost is a name that usually resolves to both; a server bound to one of them does not answer on the other.

Does --host fix it?

In the tested setup yes, both addresses answered. But --host also exposes the dev server to the network, so a fixed loopback address is the narrower fix.

Can allowedHosts or CORS cause the refusal?

No. A refused connection never reaches Vite. Both checks allow localhost, 127.0.0.1 and ::1 by default.

# next

Related