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

  1. Select File > Add Package Dependencies…

  2. Enter the Apollo iOS package URL (https://github.com/apollographql/apollo-ios.git) and click Add Package.

  3. Select the ApolloWebSocket library 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:

Swift
Package.swift
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:

Swift
1import ApolloWebSocket

Create 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):

Swift
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 ApolloStore instance to both the WebSocketTransport and ApolloClient. 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:

Swift
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:

Swift
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

OptionTypeDefaultDescription
reconnectionIntervalTimeInterval-1 (disabled)Seconds to wait before reconnecting after a disconnect. 0 reconnects immediately. Negative values disable auto-reconnection.
connectingPayloadJSONEncodableDictionary?nilPayload sent in the connection_init message. Commonly used for authentication.
pingIntervalTimeInterval?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.
requestBodyCreatorJSONRequestBodyCreatorDefaultRequestBodyCreator()Builds the JSON payload for subscribe messages.
operationMessageIdCreatorOperationMessageIdCreatorApolloSequencedOperationMessageIdCreator()Generates unique IDs for each operation message.
clientAwarenessMetadataClientAwarenessMetadataClientAwarenessMetadata()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):

Swift
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:

Swift
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:

Swift
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.

Swift
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 subscription are 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:

Swift
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:

Swift
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

StateDescription
.pendingInitiated but not yet active. The transport might be connecting or sending the subscribe message.
.activeActive. The subscription can receive data from the server.
.reconnectingThe connection was lost. The transport is attempting to reconnect.
.pausedThe 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:

Swift
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 WebSocketTransport as an isolated parameter, 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 a Task inside the implementation:

Swift
1func webSocketTransportDidConnect(_ transport: isolated WebSocketTransport) {
2  Task { await self.updateUI() }
3}

Delegate methods

MethodDescription
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:

Swift
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:

Swift
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:

Swift
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:

Swift
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}
Feedback

Edit on GitHub

Ask Community