Hi, On Windows, curl against a service listening only on 127.0.0.1 costs ~212ms via "localhost" and ~3-9ms via "127.0.0.1". That is the problem from #14144 / #2281 and from
https://daniel.haxx.se/blog/2024/08/14/slow-tcp-connect-on-windows/ I agree with the not-a-curl-bug call on #14144: the ~2s that Windows takes to report a refused connect is platform behaviour, not a curl defect. I am not asking to revisit that. What I would like to raise is a per-socket opt-out that does not appear in either issue or in the blog post. libuv has shipped it since 2018, it needs no detection of the stall, and it takes the refusal from ~2000ms to ~0.3ms. The mechanism ------------- The stall is the SYN retransmission budget. Isolated with raw Winsock (ConnectEx + IOCP) against a closed loopback port on Windows 11, varying only TCP_INITIAL_RTO_PARAMETERS.MaxSynRetransmissions via SIO_TCP_INITIAL_RTO: no ioctl (system default = 4) 2026 ms 0 (DEFAULT) 2015 ms 1 506 ms 255 (UNSPECIFIED) 2023 ms 254 (NO_SYN_RETRANSMISSIONS) 0.3 ms Linear at ~505ms per retransmission. Reproduced on two independent Windows machines. The sentinel encoding in mstcpip.h is: 0 means "default", 255 means "unspecified", and 254 is the one that means "none". This cannot be pushed onto users as a Windows configuration change. The TCP template surface (Set-NetTCPSetting bound to a prefix by New-NetTransportFilter) accepts MaxSynRetransmissions only in the range 2-8 and rejects 254 outright, so the best achievable system-wide is ~1010ms. Zero is reachable only per-socket, which is why it has to be done in the application. curl is still affected ---------------------- curl 8.21.0 (Windows) libcurl/8.21.0 Schannel zlib/1.3.2 WinIDN Release-Date: 2026-06-24 Against a dev server bound to 127.0.0.1 only: http://localhost:4200/ 212-225 ms, consistently http://127.0.0.1:4200/ 3-9 ms The ~212ms is the 200ms happy eyeballs default plus the actual connect. Same figures on curl 8.10.1 (mingw). On the same machine and the same ports, node reports ECONNREFUSED in 3ms, solely because libuv sets this ioctl. Proposal -------- In cf_socket_open(), or immediately before do_connect(), on Windows 10 1709+, when the destination address is loopback: TCP_INITIAL_RTO_PARAMETERS rto; memset(&rto, 0, sizeof(rto)); rto.Rtt = TCP_INITIAL_RTO_DEFAULT_RTT; rto.MaxSynRetransmissions = TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS; WSAIoctl(sockfd, SIO_TCP_INITIAL_RTO, &rto, sizeof(rto), NULL, 0, &bytes, NULL, NULL); Reference implementation, libuv src/win/tcp.c, uv__tcp_try_connect(): if (uv__windows10_version1709() && uv__is_loopback(&converted)) { memset(&retransmit_ioctl, 0, sizeof(retransmit_ioctl)); retransmit_ioctl.Rtt = TCP_INITIAL_RTO_DEFAULT_RTT; retransmit_ioctl.MaxSynRetransmissions = TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS; WSAIoctl(handle->socket, SIO_TCP_INITIAL_RTO, &retransmit_ioctl, sizeof(retransmit_ioctl), NULL, 0, &bytes, NULL, NULL); } On loopback there is no medium to lose a SYN on, so retransmission is pure cost. Failure of the ioctl is safely ignorable - it is an optimisation, and older Windows simply keeps today's behaviour. Effect ------ The IPv6 attempt to [::1] fails in ~0.3ms rather than stalling, so the IPv4 attempt starts immediately instead of after the happy eyeballs timer: roughly 212ms -> ~1ms for the common case of a service bound to IPv4 only. It also helps outside happy eyeballs. Any connect to a closed loopback port, --ipv4 included, currently costs ~2s and would cost ~0.3ms. That matters for scripts that poll a port waiting for a service to come up. Notes ----- 1) CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS = 0 works, but: - every user has to know about it and opt in per invocation; - it disables happy eyeballs staggering for all destinations, including real network ones where the stagger is doing useful work; - it does not help the non-happy-eyeballs cases. The ioctl is automatic, scoped to loopback, and leaves behaviour on real networks untouched. 2) Requires Windows 10 1709+ for the TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS sentinel; SIO_TCP_INITIAL_RTO itself is older, so a configure/header check is needed for the constant. Regards, Marcel
-- Unsubscribe: https://lists.haxx.se/mailman/listinfo/curl-library Etiquette: https://curl.se/mail/etiquette.html
