
The ghost of ::1 — when localhost hung and 127.0.0.1 didn't
The service that answered “sometimes”
There was a service in my workflow that I learned to distrust: Qdrant, on port 6333. Most of the time it answered right away. Every now and then — for no apparent reason — the connection simply hung. A command that ran in 200ms would stall until timeout.
The worst part was that it was intermittent. I’d try again, and it worked. Try again, it hung. Restart the app, it worked. Five minutes later, it hung again. For a vector database that the pipeline queries all the time, this is the kind of instability that drives you crazy: it’s not a crash, it’s not an error — it’s a ghost.
And it wasn’t just Qdrant. Local helper APIs showed the same pattern. Anything using localhost in the connection had a chance of hanging.
Hunting the ghost
The first obvious suspect: the service. Maybe Qdrant was crashing, maybe memory was blowing up, maybe WSL was going to sleep. I went down that path and found nothing — the process was up, healthy, responding.
Until one day, with the command stuck on screen, I did the simplest test in the world: I swapped localhost for 127.0.0.1.
# Hung (again)
curl -v http://localhost:6333/health
# Answered instantly
curl -v http://127.0.0.1:6333/health
Instant response. HTTP 200, health check body, zero drama. Same service, same process, same port — only the hostname changed.
The explanation: Windows resolves localhost to ::1
Then came the part that explains everything: on Windows, localhost is not a guaranteed IPv4 alias. The system resolves localhost to IPv6 ::1 first — and if the IPv6 loopback connection doesn’t complete (IPv6 stack disturbed by a VPN, service listening on IPv4 only, networking config in the way), the connection hangs. It doesn’t refuse, it doesn’t fail fast: it stays stuck trying ::1, until it gives up.
127.0.0.1 cuts that ambiguity at the root: it’s a literal IPv4 address, no name resolution, no “first” or “second” — it goes straight to IPv4 loopback, which was working all along.
# On Windows, ping localhost shows the ghost:
# Reply from ::1: time<1ms ← IPv6 first
# With the literal address, IPv4 answers:
ping 127.0.0.1
The pattern I saw as “intermittent” wasn’t the service fluctuating. It was name resolution fluctuating: sometimes the system completed the IPv6 path, sometimes it didn’t. The service was never sick — the address was a lottery.
The rule of 88
After the discovery, I started logging every occurrence. Every time a command with localhost hung and the same command with 127.0.0.1 answered instantly, I recorded it. 88 times. 88 confirmations that the problem wasn’t the service, wasn’t the network, wasn’t the time of day — it was the name localhost.
From then on it became a rule: always use 127.0.0.1. In every local service config, every API endpoint, every connection script:
# Qdrant / clients config
host: 127.0.0.1
port: 6333
No more localhost in local production. The name is comfortable, but on Windows it carries an ambiguity that costs minutes of debugging every day.
The lessons that stuck
localhost≠127.0.0.1on Windows — the system resolveslocalhostto IPv6::1first. If the IPv6 path fails, the connection hangs instead of refusing.- An intermittent network symptom can be IPv6 loopback — when a local service “sometimes hangs, sometimes works”, suspect name resolution before blaming the process.
- A literal address removes the lottery —
127.0.0.1is pure IPv4: no DNS, no address family preference, no surprise. - A simple A/B test solves it — before diving into logs and profiling, swap the hostname for the literal address and see if the symptom disappears.
Technical details
| Scenario | Result |
|---|---|
curl http://localhost:6333/health |
Intermittent hang (timeout) |
curl http://127.0.0.1:6333/health |
Instant HTTP 200 |
localhost resolution on Windows |
IPv6 ::1 first |
127.0.0.1 |
Literal IPv4 address, no resolution |
| Pattern confirmations (hung ↔ answered) | 88 times |
| Adopted rule | 127.0.0.1 always, never localhost |
The ghost was never the service. It was a six-character address that Windows insisted on resolving to the wrong side of the address family. Once the rule stuck, the timeouts disappeared — and I never wrote localhost in a local service config again.