Use when implementing Network.framework connections, debugging connection failures, migrating from sockets/URLSession streams, or adopting structured concurrency networking patterns - prevents deprecated API usage, reachability anti-patterns, and thread-safety violations with iOS 12-26+ APIs
Use when implementing Network.framework connections, debugging connection failures, migrating from sockets/URLSession streams, or adopting structured concurrency networking patterns - prevents deprecated API usage, reachability anti-patterns, and thread-safety violations with iOS 12-26+ APIs
Requesting code review of networking implementation before shipping
Related Skills
Use axiom-networking-diag for systematic troubleshooting of connection failures, timeouts, and performance issues
Use for comprehensive API reference with all WWDC examples
axiom-network-framework-ref
Example Prompts
1. "How do I migrate from SCNetworkReachability? My app checks connectivity before connecting."
2. "My connection times out after 60 seconds. How do I debug this?"
3. "Should I use NWConnection or NetworkConnection? What's the difference?"
Red Flags — Anti-Patterns to Prevent
If you're doing ANY of these, STOP and use the patterns in this skill:
❌ CRITICAL — Never Do These
1. Using SCNetworkReachability to check connectivity before connecting
// ❌ WRONG — Race conditionifSCNetworkReachabilityGetFlags(reachability, &flags) {
connection.start() // Network may change between check and start
}
Why this fails Network state changes between reachability check and connect(). You miss Network.framework's smart connection establishment (Happy Eyeballs, proxy handling, WiFi Assist). Apple deprecated this API in 2018.
Why this fails NetworkConnection designed for pure async/await. Mixing paradigms creates difficult error propagation and cancellation issues.
8. Not supporting network transitions
// ❌ WRONG — Connection fails on WiFi → cellular transition// No viabilityUpdateHandler, no betterPathUpdateHandler// User walks out of building → connection dies
Why this fails Modern apps must handle network changes gracefully. 40% of connection failures happen during network transitions.
Mandatory First Steps
ALWAYS complete these steps before writing any networking code:
// Step 1: Identify your use case// Record: "UDP gaming" vs "TLS messaging" vs "Custom protocol over QUIC"// Ask: What data am I sending? Real-time? Reliable delivery needed?// Step 2: Check if URLSession is sufficient// URLSession handles: HTTP, HTTPS, WebSocket, TCP/TLS streams (via StreamTask)// Network.framework handles: UDP, custom protocols, low-level control, peer-to-peer// If HTTP/HTTPS/WebSocket → STOP, use URLSession instead// Example:URLSession.shared.dataTask(with: url) { ... } // ✅ Correct for HTTP// Step 3: Choose API version based on deployment targetif#available(iOS26, *) {
// Use NetworkConnection (structured concurrency, async/await)// TLV framing built-in, Coder protocol for Codable types
} else {
// Use NWConnection (completion handlers)// Manual framing or custom framers
}
// Step 4: Verify you're NOT using deprecated APIs// Search your codebase for these:// - SCNetworkReachability → Use connection waiting state// - CFSocket → Use NWConnection// - NSStream, CFStream → Use NWConnection// - NSNetService → Use NWBrowser or NetworkBrowser// - getaddrinfo → Let Network.framework handle DNS// To search:// grep -rn "SCNetworkReachability\|CFSocket\|NSStream\|getaddrinfo" .
What this tells you
If HTTP/HTTPS: Use URLSession, not Network.framework
If iOS 26+ deployment: Use NetworkConnection with async/await
If iOS 12-25 support needed: Use NWConnection with completion handlers
If any deprecated API found: Must migrate before shipping (App Store review concern)
Decision Tree
Use this to select the correct pattern in 2 minutes:
import Network
// Basic connection with TLSlet connection =NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLS() // TCP and IP inferred automatically
}
// Send and receive with async/awaitpublicfuncsendAndReceiveWithTLS() asyncthrows {
let outgoingData =Data("Hello, world!".utf8)
tryawait connection.send(outgoingData)
let incomingData =tryawait connection.receive(exactly: 98).content
print("Received data: \(incomingData)")
}
// Optional: Monitor connection state for UI updatesTask {
forawait state in connection.states {
switch state {
case .preparing:
print("Establishing connection...")
case .ready:
print("Connected!")
case .waiting(let error):
print("Waiting for network: \(error)")
case .failed(let error):
print("Connection failed: \(error)")
case .cancelled:
print("Connection cancelled")
@unknowndefault:
break
}
}
}
Custom parameters for low data mode
let connection =NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029),
using: .parameters {
TLS {
TCP {
IP()
.fragmentationEnabled(false)
}
}
}
.constrainedPathsProhibited(true) // Don't use cellular in low data mode
)
Use Instruments Network template to profile datagram throughput
Check for packet loss with receive timeouts
Test on cellular network (higher latency/loss)
Pattern 1c: TLV Framing (iOS 26+)
Use when Need message boundaries on stream protocols (TCP/TLS), have mixed message types, want type-safe message handling
Time cost 15-20 minutes
Background Stream protocols (TCP/TLS) don't preserve message boundaries. If you send 3 chunks, receiver might get them 1 byte at a time, or all at once. TLV (Type-Length-Value) solves this by encoding each message with its type and length.
❌ BAD: Manual Length Prefix Parsing
// WRONG — Error-prone, boilerplate-heavylet lengthData =tryawait connection.receive(exactly: 4).content
let length = lengthData.withUnsafeBytes { $0.load(as: UInt32.self) }
let messageData =tryawait connection.receive(exactly: Int(length)).content
// Now decode manually...
✅ GOOD: TLV Framing with Type Safety
import Network
// Define your message typesenumGameMessage: Int {
case selectedCharacter =0case move =1
}
structGameCharacter: Codable {
let character: String
}
structGameMove: Codable {
let row: Intlet column: Int
}
// Connection with TLV framinglet connection =NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
TLV {
TLS()
}
}
// Send typed messagespublicfuncsendWithTLV() asyncthrows {
let characterData =tryJSONEncoder().encode(GameCharacter(character: "🐨"))
tryawait connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
}
// Receive typed messagespublicfuncreceiveWithTLV() asyncthrows {
let (incomingData, metadata) =tryawait connection.receive()
switchGameMessage(rawValue: metadata.type) {
case .selectedCharacter:
let character =tryJSONDecoder().decode(GameCharacter.self, from: incomingData)
print("Character selected: \(character)")
case .move:
let move =tryJSONDecoder().decode(GameMove.self, from: incomingData)
print("Move: row=\(move.row), column=\(move.column)")
case .none:
print("Unknown message type: \(metadata.type)")
}
}
When to use
Mixed message types in same connection (chat + presence + typing indicators)
Existing protocols using TLV (many custom protocols)
Need message boundaries without heavy framing overhead
How it works
Type: UInt32 message identifier (your enum raw value)
Length: UInt32 message size (automatic)
Value: Actual message bytes
Performance characteristics
Minimal overhead: 8 bytes per message (type + length)
No manual parsing: Framework handles framing
Type-safe: Compiler catches message type errors
Pattern 1d: Coder Protocol (iOS 26+)
Use when Sending/receiving Codable types, want to eliminate JSON boilerplate, need type-safe message handling
Time cost 10-15 minutes
Background Most apps manually encode Codable types to JSON, send bytes, receive bytes, decode JSON. Coder protocol eliminates this boilerplate by handling serialization automatically.
❌ BAD: Manual JSON Encoding/Decoding
// WRONG — Boilerplate-heavy, error-pronelet encoder =JSONEncoder()
let data =try encoder.encode(message)
tryawait connection.send(data)
let receivedData =tryawait connection.receive().content
let decoder =JSONDecoder()
let message =try decoder.decode(GameMessage.self, from: receivedData)
✅ GOOD: Coder Protocol for Direct Codable Send/Receive
import Network
// Define message types as Codable enumenumGameMessage: Codable {
case selectedCharacter(String)
case move(row: Int, column: Int)
}
// Connection with Coder protocollet connection =NetworkConnection(
to: .hostPort(host: "www.example.com", port: 1029)
) {
Coder(GameMessage.self, using: .json) {
TLS()
}
}
// Send Codable types directlypublicfuncsendWithCoder() asyncthrows {
let selectedCharacter: GameMessage= .selectedCharacter("🐨")
tryawait connection.send(selectedCharacter) // No encoding needed!
}
// Receive Codable types directlypublicfuncreceiveWithCoder() asyncthrows {
let gameMessage =tryawait connection.receive().content // Returns GameMessage!switch gameMessage {
case .selectedCharacter(let character):
print("Character selected: \(character)")
case .move(let row, let column):
print("Move: (\(row), \(column))")
}
}
Pattern 2d: Network Discovery (NWBrowser for service discovery)
Pressure Scenarios
Scenario 1: Reachability Race Condition Under App Store Deadline
Context
You're 3 days from App Store submission. QA reports connection failures on cellular networks (15% failure rate). Your PM reviews the code and suggests: "Just add a reachability check before connecting. If there's no network, show an error immediately instead of timing out."
Pressure signals
⏰ Deadline pressure "App Store deadline is Friday. We need this fixed by EOD Wednesday."
👔 Authority pressure PM (non-technical) suggesting specific implementation
📊 Customer impact "15% of users affected, mostly on cellular"
Rationalization trap
"SCNetworkReachability is Apple's API, it must be correct. I've seen it in Stack Overflow answers with 500+ upvotes. Adding a quick reachability check will fix the issue today, and I can refactor it properly after launch. The deadline is more important than perfect code right now."
Why this fails
Race condition Network state changes between reachability check and connection start. You check "WiFi available" at 10:00:00.000, but WiFi disconnects at 10:00:00.050, then you call connection.start() at 10:00:00.100. Connection fails, but reachability said it was available.
Misses smart connection establishment Network.framework tries multiple strategies (IPv4, IPv6, proxies, WiFi Assist fallback to cellular). SCNetworkReachability gives you "yes/no" but doesn't tell you which strategy will work.
Deprecated API Apple explicitly deprecated SCNetworkReachability in WWDC 2018. App Store Review may flag this as using legacy APIs.
Doesn't solve actual problem 15% cellular failures likely caused by not handling waiting state, not by absence of reachability check.
MANDATORY response
// ❌ NEVER check reachability before connecting/*
if SCNetworkReachabilityGetFlags(reachability, &flags) {
if flags.contains(.reachable) {
connection.start()
} else {
showError("No network") // RACE CONDITION
}
}
*/// ✅ ALWAYS let Network.framework handle waiting statelet connection =NWConnection(
host: NWEndpoint.Host("api.example.com"),
port: NWEndpoint.Port(integerLiteral: 443),
using: .tls
)
connection.stateUpdateHandler = { [weakself] state inswitch state {
case .preparing:
// Show: "Connecting..."self?.showStatus("Connecting...")
case .ready:
// Connection establishedself?.hideStatus()
self?.sendRequest()
case .waiting(let error):
// CRITICAL: Don't fail here, show "Waiting for network"// Network.framework will automatically retry when network returnsprint("Waiting for network: \(error)")
self?.showStatus("Waiting for network...")
// User walks out of elevator → WiFi returns → automatic retrycase .failed(let error):
// Only fail after framework exhausts all options// (tried IPv4, IPv6, proxies, WiFi Assist, waited for network)print("Connection failed: \(error)")
self?.showError("Connection failed. Please check your network.")
case .cancelled:
self?.hideStatus()
@unknowndefault:
break
}
}
connection.start(queue: .main)
Professional push-back template
"I understand the deadline pressure. However, adding SCNetworkReachability will create a race condition that will make the 15% failure rate worse, not better. Apple deprecated this API in 2018 specifically because it causes these issues.
The correct fix is to handle the waiting state properly, which Network.framework provides. This will actually solve the cellular failures because the framework will automatically retry when network becomes available (e.g., user walks out of elevator, WiFi returns).
Implementation time: 15 minutes to add waiting state handler vs 2-4 hours debugging reachability race conditions. The waiting state approach is both faster AND more reliable."
Time saved
Reachability approach 30 min to implement + 2-4 hours debugging race conditions + potential App Store rejection = 3-5 hours total
Waiting state approach 15 minutes to implement + 0 hours debugging = 15 minutes total
Savings 2.5-4.5 hours + avoiding App Store review issues
Actual root cause of 15% cellular failures
Likely missing waiting state handler. When user is in area with weak cellular, connection moves to waiting state. Without handler, app shows "Connection failed" instead of "Waiting for network," so user force-quits and reports "doesn't work on cellular."
Scenario 2: Blocking Socket Call Causing Main Thread Hang
Context
Your app has 1-star reviews: "App freezes for 5-10 seconds randomly." After investigation, you find a "quick" socket connect() call on the main thread. Your tech lead says: "This is a legacy code path from 2015. It only connects to localhost (127.0.0.1), so it should be instant. The real fix is a 3-week refactor to move all networking to a background queue, but we don't have time. Just leave it for now."
Pressure signals
⏰ Time pressure "3-week refactor, we're in feature freeze for 2.0 launch"
💸 Sunk cost "This code has worked for 8 years, why change it now?"
🎯 Scope pressure "It's just localhost, not a real network call"
📊 Low frequency "Only 2% of users see this freeze"
Rationalization trap
"Connecting to localhost is basically instant. The freeze must be caused by something else. Besides, refactoring this legacy code is risky—what if I break something? Better to leave working code alone and focus on the new features for 2.0."
Why this fails
Even localhost can block If the app has many threads, the kernel may schedule other work before returning from connect(). Even 50-100ms is visible to users as a stutter.
ANR (Application Not Responding) iOS watchdog will terminate your app if main thread blocks for >5 seconds. This explains "random" crashes.
Localhost isn't always available If VPN is active, localhost routing can be delayed. If device is under memory pressure, kernel scheduling is slower.
Guaranteed App Store rejection Apple's App Store Review Guidelines explicitly check for main thread blocking. This will fail App Review's performance tests.
MANDATORY response
// ❌ NEVER call blocking socket APIs on main thread/*
let sock = socket(AF_INET, SOCK_STREAM, 0)
connect(sock, &addr, addrlen) // BLOCKS MAIN THREAD → ANR
*/// ✅ ALWAYS use async connection, even for localhostfuncconnectToLocalhost() {
let connection =NWConnection(
host: "127.0.0.1",
port: 8080,
using: .tcp
)
connection.stateUpdateHandler = { [weakself] state inswitch state {
case .ready:
print("Connected to localhost")
self?.sendRequest(on: connection)
case .failed(let error):
print("Localhost connection failed: \(error)")
default:
break
}
}
// Non-blocking, returns immediately
connection.start(queue: .main)
}
Alternative: If you must keep legacy socket code (not recommended)
// Move blocking call to background queue (minimum viable fix)DispatchQueue.global(qos: .userInitiated).async {
let sock = socket(AF_INET, SOCK_STREAM, 0)
connect(sock, &addr, addrlen) // Still blocks, but not main threadDispatchQueue.main.async {
// Update UI after connection
}
}
Professional push-back template
"I understand this code has been stable for 8 years. However, Apple's App Store Review now runs automated performance tests that will fail apps with main thread blocking. This will block our 2.0 release.
The fix doesn't require a 3-week refactor. I can wrap the existing socket code in a background queue dispatch in 30 minutes. Or, I can replace it with NWConnection (non-blocking) in 45 minutes, which also eliminates the socket management code entirely.
Neither approach requires touching other parts of the codebase. We can ship 2.0 on schedule AND fix the ANR crashes."
Time saved
Leave it alone 0 hours upfront + 4-8 hours when App Review rejects + user churn from 1-star reviews
Background queue fix 30 minutes = main thread safe
Scenario 3: Design Review Pressure — "Use WebSockets for Everything"
Context
Your team is building a multiplayer game with real-time player positions (20 updates/second). In architecture review, the senior architect says: "All our other apps use WebSockets for networking. We should use WebSockets here too for consistency. It's production-proven, and the backend team already knows how to deploy WebSocket servers."
Pressure signals
👔 Authority pressure Senior architect with 15 years experience
🏢 Org consistency "All other apps use WebSockets"
💼 Backend expertise "Backend team doesn't know UDP"
📊 Proven technology "WebSockets are battle-tested"
Rationalization trap
"The architect has way more experience than me. If WebSockets work for the other apps, they'll work here too. UDP sounds complicated and risky. Better to stick with proven technology than introduce something new that might break in production."
Why this fails for real-time gaming
Head-of-line blocking WebSockets use TCP. If one packet is lost, TCP blocks ALL subsequent packets until retransmission succeeds. In a game, this means old player position (frame 100) blocks new position (frame 120), causing stutter.
Latency overhead TCP requires 3-way handshake (SYN, SYN-ACK, ACK) before sending data. For 20 updates/second, this overhead adds 50-150ms latency.
Unnecessary reliability Game position updates don't need guaranteed delivery. If frame 100 is lost, frame 101 (5ms later) makes it obsolete. TCP retransmits frame 100, wasting bandwidth.
Connection establishment WebSockets require HTTP upgrade handshake (4 round trips) before data transfer. UDP starts sending immediately.
MANDATORY response
// ❌ WRONG for real-time gaming/*
let webSocket = URLSession.shared.webSocketTask(with: url)
webSocket.resume()
webSocket.send(.data(positionUpdate)) { error in
// TCP guarantees delivery but blocks on loss
// Old position blocks new position → stutter
}
*/// ✅ CORRECT for real-time gaminglet connection =NWConnection(
host: NWEndpoint.Host("game-server.example.com"),
port: NWEndpoint.Port(integerLiteral: 9000),
using: .udp
)
connection.stateUpdateHandler = { state inifcase .ready = state {
print("Ready to send game updates")
}
}
connection.start(queue: .main)
// Send player position updates (20/second)funcsendPosition(_position: PlayerPosition) {
let data = encodePosition(position)
connection.send(content: data, completion: .contentProcessed { error in// Fire and forget, no blocking// If this frame is lost, next frame (50ms later) makes it obsolete
})
}
Technical comparison table
Aspect
WebSocket (TCP)
UDP
Latency (typical)
50-150ms
10-30ms
Head-of-line blocking
Yes (old data blocks new)
No
Connection setup
4 round trips (HTTP upgrade)
0 round trips
Packet loss handling
Blocks until retransmit
Continues with next packet
Bandwidth (20 updates/sec)
~40 KB/s
~20 KB/s
Best for
Chat, API calls
Gaming, streaming
Professional push-back template
"I appreciate the concern about consistency and proven technology. WebSockets are excellent for our other apps because they're doing chat, notifications, and API calls—use cases where guaranteed delivery matters.
However, real-time gaming has different requirements. Let me explain with a concrete example:
Player moves from position A to B to C (3 updates in 150ms). With WebSockets:- Frame A sent- Frame A packet lost- Frame B sent, but TCP blocks it (waiting for Frame A retransmit)- Frame C sent, also blocked- Frame A retransmits, arrives 200ms later- Frames B and C finally delivered- Result: 200ms of frozen player position, then sudden jump to C
With UDP:- Frame A sent and lost- Frame B sent and delivered (50ms later)- Frame C sent and delivered (50ms later)- Result: Smooth position updates, no freeze
The backend team doesn't need to learn UDP from scratch—they can use the same Network.framework on server-side Swift (Vapor, Hummingbird). Implementation time is the same.
I'm happy to do a proof-of-concept this week showing latency comparison. We can measure both approaches with real data."
When WebSockets ARE correct
Chat applications (message delivery must be reliable)
Not using SCNetworkReachability anywhere in codebase
Not using CFSocket, NSSocket, or BSD sockets directly
Not using NSStream or CFStream
Not using NSNetService (use NWBrowser instead)
Not calling getaddrinfo for manual DNS resolution
Connection Configuration
Using hostname, NOT hardcoded IP address
TLS enabled for sensitive data (passwords, tokens, user content)
Handling waiting state with user feedback ("Waiting for network...")
Not checking reachability before calling connection.start()
Memory Management
Using [weak self] in all NWConnection completion handlers
Or using NetworkConnection (iOS 26+) with async/await (no [weak self] needed)
Calling connection.cancel() when done to free resources
Network Transitions
Supporting network changes (WiFi → cellular, or vice versa)
Using viabilityUpdateHandler or betterPathUpdateHandler (NWConnection)
Or monitoring connection.states async sequence (NetworkConnection)
NOT tearing down connection immediately on viability change
Testing on Real Devices
Tested on real device (not just simulator)
Tested WiFi → cellular transition (walk out of building)
Tested Airplane Mode toggle (enable → disable)
Tested on IPv6-only network (some cellular carriers)
Tested with corporate VPN active
Tested with low signal (basement, elevator)
Performance
Using connection.batch for multiple UDP datagrams (30% CPU reduction)
Using contentProcessed completion for send pacing (not sleep())
Profiled with Instruments Network template
Connection establishment < 500ms (check with logging)
Error Handling
Handling .failed state with specific error
Timeout handling (don't wait forever in .preparing)
TLS handshake errors logged for debugging
User-facing errors are actionable ("Check network" not "POSIX error 61")
iOS 26+ Features (if using NetworkConnection)
Using TLV framing if need message boundaries
Using Coder protocol if sending Codable types
Using NetworkListener instead of NWListener
Using NetworkBrowser with Wi-Fi Aware for peer-to-peer
Real-World Impact
User-Space Networking: 30% CPU Reduction
WWDC 2018 Demo Live UDP video streaming comparison:
BSD sockets ~30% higher CPU usage on receiver
Network.framework ~30% lower CPU usage
Why Traditional sockets copy data kernel → userspace. Network.framework uses memory-mapped regions (no copy) and reduces context switches from 100 syscalls → ~1 syscall (with batching).
Impact for your app
Lower battery drain (30% less CPU = longer battery life)
Smoother gameplay (more CPU for rendering)
Cooler device (less thermal throttling)
Smart Connection Establishment: 50% Faster
Traditional approach
Call getaddrinfo (100-300ms DNS lookup)
Try first IPv6 address, wait 5 seconds for timeout
Try IPv4 address, finally connects
Network.framework (Happy Eyeballs)
Start DNS lookup in background
As soon as first address arrives, try connecting
Start second connection attempt 50ms later
Use whichever connects first
Result 50% faster connection establishment in dual-stack environments (measured by Apple)
Proper State Handling: 10x Crash Reduction
Customer report App crash rate dropped from 5% → 0.5% after implementing waiting state handler.
Before App showed "Connection failed" when no network, users force-quit app → crash report.
After App showed "Waiting for network" and automatically retried when WiFi returned → users saw seamless reconnection.
Last Updated 2025-12-02
Status Production-ready patterns from WWDC 2018 and WWDC 2025
Tested Patterns validated against Apple documentation and WWDC transcripts