How does URLSessionWebSocketTask work, and what do you have to handle yourself?

12.4.1 · L3 · Interview

Predict first

A new task and the close codes:

import Foundation

let task = URLSession.shared.webSocketTask(with: URL(string: "wss://example.test/ws")!)
print(task.closeCode.rawValue, task.state.rawValue)
task.cancel(with: .goingAway, reason: nil)
print(URLSessionWebSocketTask.CloseCode.normalClosure.rawValue, URLSessionWebSocketTask.CloseCode.goingAway.rawValue)
  • 0 1, then 1000 1001
  • 1000 0, then 0 1
  • 0 0, then 1 2

Mental model. HTTP is sending letters; a WebSocket is a phone call you keep open. The call needs someone to redial when it drops, to say “where were we?”, and it does not ring when you have put the phone away (the app is suspended).

A WebSocket is a long-lived, bidirectional connection that starts as an HTTP request (an upgrade) and then carries framed text or binary messages both ways without per-message HTTP overhead; use it for chat, live prices, presence and collaborative state. URLSession.webSocketTask(with:) returns a URLSessionWebSocketTask in the suspended state, and nothing connects until you call resume(). You send with send(.string(...)) or send(.data(...)) and receive with receive(), which returns one message per call: you must call it again in a loop (while true { let m = try await task.receive() … }) or you stop hearing messages. Wrap that loop in an AsyncThrowingStream (or a task) so consumers can for try await it, and cancel the task when the stream ends. Use sendPing to check the connection is alive, and close with cancel(with: .normalClosure, reason:) (close code 1000; 1001 is “going away”).

What the API does not do for you: reconnect, heartbeats beyond a manual ping, message framing of your protocol, ordering and delivery guarantees across reconnects, or backpressure. A production client adds: reconnection with exponential backoff and jitter, resubscription of what the server needs to know after a reconnect, a sequence number or “last event id” to resume without gaps or duplicates, a decision about what to do with messages sent while offline (queue, drop or fail), authentication on the handshake (headers on the request) and token refresh, and an app-lifecycle policy: iOS suspends the app soon after it goes to the background, which closes the socket, so use push notifications for background delivery, not a socket. Remember that proxies and load balancers close idle connections, so a periodic ping keeps one open. For simple server-to-client updates, Server-Sent Events over a streaming URLSession request may be simpler than a WebSocket, and for high-level needs consider a library that handles reconnects.

A fresh task has no close code (0) and is suspended (state 1). The standard close codes are 1000 and 1001.

Common traps

  • Calling receive() once. It returns one message; loop, or you miss everything after the first.
  • Assuming the socket survives backgrounding. The app is suspended and the connection closes; use push for background updates.

They might follow up with

  • What state does a new web socket task start in? Suspended; call resume() to connect.
  • How do you resume without gaps after a reconnect? Send a last-seen sequence number or event id and let the server replay.

Related questions

  • 12.5.1 How should a client retry failed requests: what to retry, how long to wait, and what idempotency has to do…
  • 12.4.3 Why use NWPathMonitor instead of reachability, and how do you use connectivity information correctly?
  • 10.6.2 How do you bridge repeated events, GCD and Combine into async code with AsyncStream?

How do you do multipart uploads, large uploads and downloads, and… →