# On Windows, a refused connection to 127.0.0.1 takes 2 seconds, except in Node.js

> On Windows a connection to a closed port on 127.0.0.1 is refused only after 2 s in curl before 8.22.0, Python and .NET, because Windows retries the SYN. Node.js gets the refusal in 1 ms, Linux in 0.2 ms. Measured by AO, 2026-10-08.

AO's plugin tests use `http://127.0.0.1:9` as "a server that is down": nothing listens on port 9, so the connection is refused at once. On Linux it is. On Windows, one of those tests kept failing its 4.5 s limit whenever the PC was busy, and the reason turned out to be the refusal itself: it took two seconds.

**What we measured, on one Windows 11 PC, 2026-10-08:**

- curl 8.19.0 and 8.21.0: 2.02 to 2.05 s.
- Python 3.14.6: 2.02 to 2.03 s.
- .NET (PowerShell 5.1): 2.02 to 2.10 s.
- Node.js 24.17.0: 1 to 5 ms.
- Linux (WSL 2 on the same PC), curl 8.18.0: 0.2 ms.

**Why.** When the reset comes back, Windows does not give up: it sends the SYN again, a few times, before it reports the refusal. A program can turn that off for one socket with the `SIO_TCP_INITIAL_RTO` control code and `TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS`, which Microsoft documents as "the TCP SYN shouldn't be retransmitted". libuv, the library under Node.js, has done this for every loopback connect since Windows 10 1709. That is the whole difference between Node.js and the others. We checked it on a plain .NET socket:

```
$s = New-Object System.Net.Sockets.Socket('InterNetwork','Stream','Tcp')
[void]$s.IOControl([int]0x98000011, [byte[]](0,0,0xFE,0), $null)   # SIO_TCP_INITIAL_RTO, no SYN retries
$s.Connect('127.0.0.1', 9)   # refused within milliseconds, not after about 2 s
```

The four bytes are the `TCP_INITIAL_RTO_PARAMETERS` structure: a round-trip time of 0 (the default), then 0xFE, the value of `TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS`.

**A trap in curl.** With `-m 2`, curl gives up at 2.01 s and reports exit 28, a timeout. The refusal would have arrived a few milliseconds later, so a script that checks for exit 7 never sees it. curl 8.22.0 sets the option for localhost itself; the copies of curl that came with Windows and with Git for Windows on this PC were older.

**What to do in a test.** Do not count on a fast refusal on Windows. Either set a short connect timeout, which ends at the time you choose (`--connect-timeout 0.5` ended at 0.51 s), or make the connection from Node.js, or set the option yourself as above. Leave room in any wall-clock limit for about 2 s per refused connection, and 2.2 s for `localhost`, which curl tries on both ::1 and 127.0.0.1.

**How to check your own machine:**

```
curl -s -o /dev/null -w "exit=%{exitcode} total=%{time_total}\n" http://127.0.0.1:9/
node -e "const t=Date.now();require('net').connect(9,'127.0.0.1').on('error',e=>console.log(e.code,Date.now()-t,'ms'))"
```

**Not measured.** curl 8.22.0 and later, Windows versions other than 11, and other runtimes such as Go, Java and Rust.

## Evidence
- Windows 11 (10.0.26200), http://127.0.0.1:9/: curl 8.19.0 from Git for Windows and curl 8.21.0 from Windows itself both exited 7 (connection refused) after 2.02 to 2.05 s, three runs each (measured by AO, 2026-10-08)
- Same PC, same port: Python 3.14.6 socket.connect raised ConnectionRefusedError after 2,017 to 2,033 ms, and PowerShell 5.1's .NET TcpClient after 2,021 to 2,095 ms (measured by AO, 2026-10-08)
- Same PC, same port: Node.js 24.17.0 net.connect got ECONNREFUSED after 1 to 5 ms in five runs, and 1 to 13 ms on ::1 (measured by AO, 2026-10-08)
- Same PC, inside WSL 2 (Linux 6.6, curl 8.18.0): refused after 0.18 to 0.21 ms (measured by AO, 2026-10-08)
- The same .NET socket with SIO_TCP_INITIAL_RTO set to send no SYN again (0xFE) before connecting: refused after 0.5 to 1.0 ms, against 2,033 to 2,066 ms without it, three runs each (measured by AO, 2026-10-08)
- curl -m 2 on the same port ends at 2.01 s with exit 28, a timeout, so the refusal is never reported; --connect-timeout 0.5 ends at 0.51 s, also exit 28 (measured by AO, 2026-10-08)
- curl to localhost:9, which tries ::1 and 127.0.0.1: 2.24 to 2.25 s (measured by AO, 2026-10-08)
- In AO's own tests, a hook test with a 4.5 s limit that used 127.0.0.1:9 as a server that is down failed 4 of 4 runs while the PC was at 55 to 62% CPU (AO's own notes, 2026-10-05)
- libuv, the I/O library under Node.js, sets this option on every loopback connect on Windows 10 1709 and later, so that connect() fails "instantly ... instead of waiting for 2s" (libuv source, src/win/tcp.c, read 2026-10-08)
- curl added the same option for localhost on Windows in 8.22.0, released 2026-09-02 (curl changelog; pull request 22494, closed on 2026-08-06 when its change landed); AO has not measured 8.22.0

Sources:
- libuv — src/win/tcp.c, uv__tcp_try_connect — https://raw.githubusercontent.com/libuv/libuv/v1.x/src/win/tcp.c (accessed 2026-10-08)
- Microsoft Learn — TCP_INITIAL_RTO_PARAMETERS structure — https://learn.microsoft.com/en-us/windows/win32/api/mstcpip/ns-mstcpip-tcp_initial_rto_parameters (accessed 2026-10-08)
- Microsoft Learn — SIO_TCP_INITIAL_RTO control code — https://learn.microsoft.com/en-us/windows/win32/winsock/sio-tcp-initial-rto (accessed 2026-10-08)
- curl-library mailing list — Windows: avoiding the loopback connect stall with SIO_TCP_INITIAL_RTO (2026-08-04) — https://www.mail-archive.com/curl-library@lists.haxx.se/msg03715.html (accessed 2026-10-08)
- curl pull request 22494 — cf-socket: disable TCP SYN retransmissions for localhost on Windows — https://github.com/curl/curl/pull/22494 (accessed 2026-10-08)
- curl — changes in 8.22.0 — https://curl.se/changes.html (accessed 2026-10-08)

---
Published 2026-10-08 · windows, networking, testing · AO — Abstract Objective · https://abstractobjective.dev/knowledge/windows-refused-localhost-connection-takes-2-seconds/

The index of the whole site, for agents: https://abstractobjective.dev/llms.txt
