When do you use Network.framework (NWConnection, NWListener, NWPathMonitor) instead of URLSession, and how does a connection move through its states?
Predict first
A loopback echo server and client. What happens?
import Foundation
import Network
let listener = try NWListener(using: .tcp, on: .any)
let ready = DispatchSemaphore(value: 0)
listener.newConnectionHandler = { conn in
conn.start(queue: .global())
conn.receive(minimumIncompleteLength: 1, maximumLength: 100) { data, _, _, _ in
let msg = String(decoding: data ?? Data(), as: UTF8.self)
conn.send(content: Data("echo:\(msg)".utf8), completion: .contentProcessed { _ in })
}
}
listener.stateUpdateHandler = { if case .ready = $0 { ready.signal() } }
listener.start(queue: .global())
ready.wait()
let conn = NWConnection(host: "127.0.0.1", port: listener.port!, using: .tcp)
let done = DispatchSemaphore(value: 0)
nonisolated(unsafe) var states: [String] = []
conn.stateUpdateHandler = { s in
switch s {
case .preparing: states.append("preparing")
case .ready:
states.append("ready")
conn.send(content: Data("hello".utf8), completion: .contentProcessed { _ in })
conn.receive(minimumIncompleteLength: 1, maximumLength: 100) { data, _, _, _ in
print("received:", String(decoding: data ?? Data(), as: UTF8.self))
done.signal()
}
default: break
}
}
conn.start(queue: .global())
done.wait()
print("states:", states)
conn.cancel(); listener.cancel()
- received: echo:hello / states: ["preparing", "ready"]
- It fails with connection refused
- received: hello / states: ["ready"]
Mental model. URLSession is a courier service with tracking, customs forms and insurance. Network.framework is the open road and a vehicle: you can send anything anywhere, but you pack, route and handle breakdowns yourself.
Use URLSession for anything that is HTTP: it brings caching, cookies, redirects, HTTP/2 and 3, proxies, authentication, background transfers and the system’s connection reuse. Use Network.framework when you need a transport that URLSession does not offer: a custom TCP or UDP protocol, a game or IoT link, QUIC or TLS you configure yourself, a local server (NWListener), Bonjour browsing, or path monitoring. NWConnection is state-driven and asynchronous: you create it with a host, port and parameters (.tcp, .tls, .udp, a QUIC option), set stateUpdateHandler, and start(queue:). It goes preparing → ready (or waiting when there is no route yet, failed, or cancelled); only in .ready do you send and receive. The check runs a loopback echo server and client: states ["preparing", "ready"], then “received: echo:hello”.
What you take on: framing (TCP is a byte stream, so a receive may return part of a message or several; add length prefixes or delimiters, or use NWProtocolFramer), retries and reconnection (react to waiting with betterPathUpdateHandler and viability), TLS and certificate policy, backpressure (send completion), and cancellation (cancel() releases the socket). The .waiting state is a feature: unlike a socket that fails immediately offline, the connection waits for a path and proceeds when one appears. For connectivity information use NWPathMonitor (Q323), and never block a thread on a semaphore as the check does for brevity.
The connection prepares, becomes ready, sends hello and receives the echoed reply.
Common traps
- Assuming one
receivereturns one message. TCP has no message boundaries; frame your protocol. - Sending before
.ready. Wait for the state, or queue sends until it arrives.
They might follow up with
- What is the
.waitingstate? No usable path yet; the connection keeps trying and moves to.readywhen one exists. - Can I get HTTP from NWConnection? You can speak it by hand, but you lose caching, HTTP/2 and everything else; use URLSession.
Related questions
- 12.10.2 When do you still need a URLSession delegate, and how do the async APIs and per-task delegates fit?
- 12.2.1 How do HTTP/1.1, HTTP/2 and HTTP/3 differ, and what does that mean for an iOS client?
- 12.4.1 How does URLSessionWebSocketTask work, and what do you have to handle yourself?
- 12.4.3 Why use NWPathMonitor instead of reachability, and how do you use connectivity information correctly?
When do you still need a URLSession delegate, and how do the async… →