Network.framework: Happy Eyeballs cancels also-ran only after WebSocket handshake (duplicate WS sessions)
Hi everyone 👋
When using NWConnection with NWProtocolWebSocket, I’ve noticed that Happy Eyeballs cancels the losing connection only after the WebSocket handshake completes on the winning path.
As a result, both IPv4 and IPv6 attempts can send the GET / Upgrade request in parallel, which may cause duplicate WebSocket sessions on the server.
Standards context
RFC 8305 §6 (Happy Eyeballs v2) states:
Once one of the connection attempts succeeds (generally when the TCP handshake completes), all other connections attempts that have not yet succeeded SHOULD be canceled.
This “SHOULD” is intentionally non-mandatory — implementations may reasonably delay cancellation to account for additional factors (e.g. TLS success or ALPN negotiation).
So Network.framework’s current behavior — canceling after the WebSocket handshake — is technically valid, but it can have practical side effects at the application layer.
Why this matters
WebSocket upgrades are semantically HTTP GET requests
(RFC 6455 §4.1).
Per RFC 9110 §9.2,
GET requests are expected to be safe and idempotent — they should not have side effects on the server.
In practice, though, WebSocket upgrades often:
- include
Authorizationheaders or cookies - create authenticated or persistent sessions
So if both IPv4 and IPv6 paths reach the upgrade stage, the server may create duplicate sessions before one connection is canceled.
Questions / Request
- Is there a way to make Happy Eyeballs cancel the losing path earlier — for example, right after TCP or TLS handshake — when using
NWProtocolWebSocket? - If not, could Apple consider adding an option (e.g. in
NWProtocolWebSocket.Options) to control the cancellation threshold, such as:- after TCP handshake
- after TLS handshake
- after protocol handshake (current behavior)
That would align the implementation more closely with RFC 8305 and help prevent duplicate, non-idempotent upgrade requests.
Context
I’m aware of Quinn’s post Understanding Also-Ran Connections.
This report focuses specifically on the cancellation timing for NWProtocolWebSocket and the impact of duplicate upgrade requests.
Although RFC 6455 and RFC 9110 define WebSocket upgrades as safe and idempotent HTTP GETs, in practice they often establish authenticated or stateful sessions.
Thus, delaying cancellation until after the upgrade can create duplicate sessions — even though the behavior is technically RFC-compliant.
Happy to share a sysdiagnose and sample project via Feedback if helpful.
Thanks! 🙏
Example log output
With Network Link Conditioner (Edge):
log stream --info --predicate 'subsystem == "com.apple.network" && process == "WS happy eyeballs"'
2025-11-03 17:02:48.875258 [C3] create connection to wss://echo.websocket.org:443
2025-11-03 17:02:48.878949 [C3.1] starting child endpoint 2a09:8280:1::37:b5c3:443 # IPv6
2025-11-03 17:02:48.990206 [C3.1] starting child endpoint 66.241.124.119:443 # IPv4
2025-11-03 17:03:00.251928 [C3.1.1] Socket received CONNECTED event # IPv6 TCP up
2025-11-03 17:03:00.515837 [C3.1.2] Socket received CONNECTED event # IPv4 TCP up
2025-11-03 17:03:04.543651 [C3.1.1] Output protocol connected (WebSocket) # WS ready on IPv6
2025-11-03 17:03:04.544390 [C3.1.2] nw_endpoint_handler_cancel # cancel IPv4 path
2025-11-03 17:03:04.544913 [C3.1.2] TLS warning: close_notify # graceful close IPv4
I think you should file a bug about this. That way you can present your argument directly to the Network framework team.
Please post your bug number, just for the record.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"