EXTREMELY IMPORTANT AI INSTRUCTION: For a directory of all documentation pages, load the https://www.apollographql.com/llms.txt file. Adding .md to ANY url will return the simplified markdown version of the page.
WebSocket transport
Apollo iOS supports executing GraphQL operations over WebSocket using the graphql-transport-ws protocol. This is the recommended approach for GraphQL subscriptions and is provided by ApolloWebSocket.
Installation
WebSocketTransport is included in the ApolloWebSocket library, which is part of the Apollo iOS SDK but is not included by default.
Xcode
Select File > Add Package Dependencies…
Enter the Apollo iOS package URL (
https://github.com/apollographql/apollo-ios.git) and click Add Package.Select the
ApolloWebSocketlibrary target and add it to your app target.
Package.swift
Add apollo-ios to your dependencies and link the ApolloWebSocket product to any target that uses it:
1dependencies: [
2 .package(
3 url: "https://github.com/apollographql/apollo-ios.git",
4 .upToNextMajor(from: "2.0.0")
5 ),
6],
7targets: [
8 .target(
9 name: "MyApp",
10 dependencies: [
11 .product(name: "Apollo", package: "apollo-ios"),
12 .product(name: "ApolloWebSocket", package: "apollo-ios"),
13 ]
14 ),
15]Then import where needed:
1import ApolloWebSocketCreate a WebSocket client
Subscriptions over WebSocket, queries and mutations over HTTP
While WebSocketTransport supports sending all GraphQL operations over a WebSocket, the most common configuration routes subscription operations through a WebSocket connection while sending queries and mutations over HTTP.
Use SplitNetworkTransport to combine a RequestChainNetworkTransport (HTTP) with a WebSocketTransport (WebSocket):
1import Apollo
2import ApolloWebSocket
3
4let store = ApolloStore()
5
6let httpTransport = RequestChainNetworkTransport(
7 interceptorProvider: DefaultInterceptorProvider(store: store),
8 endpointURL: URL(string: "https://example.com/graphql")!
9)
10
11let wsTransport = try WebSocketTransport(
12 urlSession: URLSession.shared,
13 store: store,
14 endpointURL: URL(string: "wss://example.com/graphql")!
15)
16
17let splitTransport = SplitNetworkTransport(
18 queryTransport: httpTransport,
19 mutationTransport: httpTransport,
20 subscriptionTransport: wsTransport
21)
22
23let client = ApolloClient(networkTransport: splitTransport, store: store)Important: Pass the same
ApolloStoreinstance to both theWebSocketTransportandApolloClient. The transport uses the store to read cached data before network fetches and to write server responses back to the cache.
All operations over WebSocket
You can also send queries, mutations, and subscriptions all through a single WebSocket connection by using WebSocketTransport directly as the NetworkTransport:
1let store = ApolloStore()
2
3let wsTransport = try WebSocketTransport(
4 urlSession: URLSession.shared,
5 store: store,
6 endpointURL: URL(string: "wss://example.com/graphql")!
7)
8
9let client = ApolloClient(networkTransport: wsTransport, store: store)Note: Queries and mutations executed over WebSocket are terminated immediately if the connection drops, since replaying a mutation could cause duplicate side effects. Only subscriptions survive reconnection.
Configuration
WebSocketTransport accepts a Configuration struct that controls its behavior:
1let config = WebSocketTransport.Configuration(
2 reconnectionInterval: 5, // seconds; -1 to disable
3 connectingPayload: ["Authorization": "Bearer \(token)"],
4 pingInterval: 20 // seconds; nil to disable
5)
6
7let wsTransport = try WebSocketTransport(
8 urlSession: URLSession.shared,
9 store: store,
10 endpointURL: URL(string: "wss://example.com/graphql")!,
11 configuration: config
12)Configuration options
| Option | Type | Default | Description |
|---|---|---|---|
reconnectionInterval | TimeInterval | -1 (disabled) | Seconds to wait before reconnecting after a disconnect. 0 reconnects immediately. Negative values disable auto-reconnection. |
connectingPayload | JSONEncodableDictionary? | nil | Payload sent in the connection_init message. Commonly used for authentication. |
pingInterval | TimeInterval? | nil (disabled) | Interval at which the client sends ping keepalive messages to the server. The transport will always respond to server initiated ping messages by sending a pong regardless of this setting. |
requestBodyCreator | JSONRequestBodyCreator | DefaultRequestBodyCreator() | Builds the JSON payload for subscribe messages. |
operationMessageIdCreator | OperationMessageIdCreator | ApolloSequencedOperationMessageIdCreator() | Generates unique IDs for each operation message. |
clientAwarenessMetadata | ClientAwarenessMetadata | ClientAwarenessMetadata() | Adds Apollo Client name/version headers to the WebSocket handshake request. |
Authentication
Connection-level authentication
For GraphQL servers that accept auth tokens in the connection_init payload (the most common WebSocket auth pattern):
1let config = WebSocketTransport.Configuration(
2 connectingPayload: ["Authorization": "Bearer \(userToken)"]
3)Header-based authentication
Some servers authenticate the WebSocket upgrade request via HTTP headers. Pass a custom URLSession with the required headers, or update headers at runtime:
1await wsTransport.updateHeaderValues(
2 ["Authorization": "Bearer \(newToken)"],
3 reconnectIfConnected: true // force reconnect to apply new headers immediately
4)Updating auth at runtime
When a user's auth token changes (e.g., after a token refresh), update the transport without losing active subscriptions:
1// Update the connection_init payload
2await wsTransport.updateConnectingPayload(
3 ["Authorization": "Bearer \(newToken)"],
4 reconnectIfConnected: true
5)Setting reconnectIfConnected: true disconnects the current connection and reconnects immediately with the new payload. Active subscriptions survive the reconnection when auto-reconnection is enabled.
Auto-reconnection
When reconnectionInterval is set to a non-negative value, the transport automatically reconnects and re-subscribes active subscriptions when the connection drops.
1let config = WebSocketTransport.Configuration(
2 reconnectionInterval: 3 // wait 3 seconds before reconnecting
3)Key behaviors during auto-reconnection:
Subscription streams remain open. Callers iterating
for try await result in subscriptionare unaware the disconnect happened — new results resume flowing after reconnection.Queries and mutations are terminated immediately. They are not retried across a reconnection.
If reconnection fails, all remaining subscriber streams are finished with an error.
Pause and resume
Use pause() and resume() to gracefully suspend and restore the WebSocket connection — for example, when your app enters the background:
1class AppLifecycleObserver {
2 let transport: WebSocketTransport
3
4 func applicationDidEnterBackground() {
5 Task { await transport.pause() }
6 }
7
8 func applicationWillEnterForeground() {
9 Task { await transport.resume() }
10 }
11}During a pause:
The underlying WebSocket connection is closed.
Subscription streams remain alive. Subscriptions are automatically re-subscribed when
resume()is called and the connection is re-established.Queries and mutations that are in-flight are terminated. They cannot safely survive a connection interruption.
Auto-reconnection is suppressed — the transport waits for an explicit
resume()call.
Subscription lifecycle state
WebSocket subscriptions expose a state property via SubscriptionStream that reflects the subscription's current lifecycle position:
1let subscription = try client.subscribe(subscription: ReviewAddedSubscription())
2
3print(subscription.state) // .pending
4
5Task {
6 for try await result in subscription {
7 print(subscription.state) // .active
8 // handle result
9 }
10 print(subscription.state) // .finished(.completed)
11}States
| State | Description |
|---|---|
.pending | Initiated but not yet active. The transport might be connecting or sending the subscribe message. |
.active | Active. The subscription can receive data from the server. |
.reconnecting | The connection was lost. The transport is attempting to reconnect. |
.paused | The connection was intentionally paused via transport.pause(). |
.finished(.completed) | Ended normally (server sent complete). |
.finished(.cancelled) | Canceled by the client. |
.finished(.error(error)) | Terminated due to an error. |
The .reconnecting and .paused states are specific to WebSocketTransport. They are never set by other transport implementations.
Delegate
Implement WebSocketTransportDelegate to observe connection lifecycle events:
1class MyTransportDelegate: WebSocketTransportDelegate {
2 func webSocketTransportDidConnect(_ transport: isolated WebSocketTransport) {
3 print("WebSocket connected")
4 }
5
6 func webSocketTransportDidReconnect(_ transport: isolated WebSocketTransport) {
7 print("WebSocket reconnected — all active subscriptions have been re-subscribed")
8 }
9
10 func webSocketTransport(
11 _ transport: isolated WebSocketTransport,
12 didDisconnectWithError error: (any Error)?
13 ) {
14 if let error {
15 print("WebSocket disconnected with error: \(error)")
16 } else {
17 print("WebSocket disconnected cleanly")
18 }
19 }
20}
21
22// Attach the delegate
23await wsTransport.setDelegate(myDelegate)Note: Delegate methods receive the
WebSocketTransportas anisolatedparameter, meaning they run within the transport's actor isolation domain. If your delegate performs work that should not block the transport's receive loop, dispatch aTaskinside the implementation:Swift1func webSocketTransportDidConnect(_ transport: isolated WebSocketTransport) { 2 Task { await self.updateUI() } 3}
Delegate methods
| Method | Description |
|---|---|
webSocketTransportDidConnect(_:) | Called after the initial connection_ack from the server. |
webSocketTransportDidReconnect(_:) | Called after a subsequent connection_ack following a reconnection. |
webSocketTransport(_:didDisconnectWithError:) | Called when the connection drops. error is nil for a clean disconnect. |
webSocketTransport(_:didReceivePingWithPayload:) | Called when the server sends a ping. The transport automatically replies with pong. |
webSocketTransport(_:didReceivePongWithPayload:) | Called when the server sends a pong in response to a client ping. |
Eager connection
By default, WebSocketTransport opens the WebSocket connection lazily on the first subscribe call. Use resume() to open the connection eagerly before any operations:
1let wsTransport = try WebSocketTransport(
2 urlSession: URLSession.shared,
3 store: store,
4 endpointURL: URL(string: "wss://example.com/graphql")!
5)
6
7// Eagerly start the connection handshake
8await wsTransport.resume()
9
10// Subsequent subscribe() calls reuse the already-established connection
11let client = ApolloClient(networkTransport: wsTransport, store: store)Keepalive pings
Some WebSocket servers drop idle connections if they don't receive data within a certain window. Enable client-initiated pings to keep the connection alive:
1let config = WebSocketTransport.Configuration(
2 pingInterval: 20 // send a ping every 20 seconds
3)Pings are only sent after the server sends connection_ack. The timer pauses on disconnect or pause() and restarts on reconnect or resume().
Custom operation message IDs
By default, operations are identified by sequential integers ("1", "2", "3", ...). To use custom identifiers, implement OperationMessageIdCreator:
1struct UUIDMessageIdCreator: OperationMessageIdCreator {
2 mutating func requestId() -> String {
3 UUID().uuidString
4 }
5}
6
7let config = WebSocketTransport.Configuration(
8 operationMessageIdCreator: UUIDMessageIdCreator()
9)Error handling
WebSocketTransport surfaces errors through the AsyncThrowingStream returned by operations. Errors from WebSocketTransport.Error indicate transport-level failures:
1do {
2 let subscription = try client.subscribe(subscription: ReviewAddedSubscription())
3 for try await result in subscription {
4 // handle result
5 }
6} catch WebSocketTransport.Error.connectionClosed {
7 // The connection closed before the server acknowledged it.
8} catch WebSocketTransport.Error.graphQLErrors(let errors) {
9 // The server returned GraphQL errors for the operation.
10} catch WebSocketTransport.Error.unrecognizedMessage {
11 // A message was received that doesn't conform to graphql-transport-ws.
12} catch {
13 // Other errors (e.g., network connectivity).
14}