Following up on TN3135 and the resolution in https://developer.apple.com/forums/thread/773362 — that thread solved establishing a low-level connection on watchOS (the asynchronous AVAudioSession.activate(options:completionHandler:) instead of the synchronous setActive()). This question is about a connection staying established, which I could not find discussed anywhere.
Environment: Apple Watch, watchOS 26.6 (23U67). Audio app, WKBackgroundModes = ["self-care"]. Real device, TestFlight build, not the simulator.
What works
Opening an NWConnection WebSocket to my own server is reliable — 8 attempts out of 8 reached .ready in 0.28–0.98 s, and an echo frame round-tripped in 27–89 ms.
Interestingly, in my measurements it opens under BOTH activation variants: the asynchronous activate(options:completionHandler:) AND the synchronous setActive(true). The two are within ~0.2 s of each other. I mention it only because the thread above concluded the synchronous one is insufficient; on 26.6 I cannot reproduce that difference for establishment.
What fails
The connection goes quiet after roughly half a minute, and an NWPathMonitor running alongside it shows why: the path transitions to .unsatisfied. Four runs:
+34.0 s (cellular) +34.6 s (cellular) +36.0 s (Wi-Fi) +34.3 s (companion link only — availableInterfaces ["other", "other"])
The server sends a heartbeat frame every 5 s and closes the socket on a schedule, so I can tell "the peer closed" from "we stopped receiving". The client receives beats 1–6 (5 s … 30 s) and then nothing; the scheduled close never arrives.
What I ruled out
- Server side. The same client construction run on macOS against the same endpoint receives all 8 heartbeats and the scheduled close at 45.1 s.
- Both audio-session activation variants — no difference, as above.
- Network type — cellular, Wi-Fi and companion-link-only all drop at ~35 s.
- The app being suspended. The app keeps logging densely throughout, and in the last run it held a WKExtendedRuntimeSession (delegate reported extendedRuntimeSessionDidStart) and was actively playing audio through AVAudioEngine from the first second — i.e. the audio-streaming condition TN3135 describes — for the entire window. The path dropped anyway, at +34.3 s.
- An idle socket. Server traffic arrives every 5 s until the drop.
The comparison that puzzles me
The same app, on the same watch, the same afternoon, relays the same realtime audio session over plain HTTPS (URLSession) instead — and that runs for 64 s continuously without a stall, including straight through a WatchConnectivity "reachability settled: unreachable" transition.
So a high-level URLSession request stream survives a period in which a low-level NWConnection's path is reported unsatisfied. That is consistent with the note in thread 773362 that "on watchOS every session is kinda like a background session, where the actual work is done out of process" — but it leaves me unsure what the intended behaviour is.
Questions
- Is a ~35 s path lifetime the expected behaviour for low-level networking on watchOS, or does it indicate something wrong on my side?
- Does the TN3135 audio-streaming exception cover only the establishment of a low-level connection, or is it also supposed to keep the path available for the duration of the audio streaming?
- If it is supposed to persist: is there something beyond an active audio session, flowing audio and a WKExtendedRuntimeSession that an app must do to keep the path alive?
- If ~35 s is the expected ceiling, is a WebSocket a supported transport for a multi-minute conversational audio session on watchOS at all — or is relaying over URLSession the intended approach despite the guidance to prefer Network framework?
Happy to file a bug with a sysdiagnose and a reduced sample project if that is more useful — please say the word and I will attach the numbers above.