Triage on main 70e5e384da: no matching duplicate found in the open-issue scan. Started the real dev server successfully and confirmed EAS simulator access, but could not execute the iOS upgrade-header comparison. The current mobile evidence workflow uses static fixtures because the cloud simulator cannot reach the VM backend; static fixtures cannot exercise WebSocket extension negotiation. Current-main iOS verification needs a simulator/device with a reachable test backend and a capture of its actual upgrade request and response. The reported packet captures remain unconfirmed here; leaving this open.
iOS app's WebSocket never negotiates permessage-deflate, so it downloads about 4× more than Android or desktop#21183
What happened
The iOS app's WebSocket never offers permessage-deflate, so the server sends it every frame uncompressed: about 4× what the Android and desktop apps download for the same messages. A ready fix, #4794, was closed in a backlog sweep on the belief that the server change (#4705) covered it. It does not.
Diagnosis
- The server compresses only when the client offers the extension (
perMessageDeflate: true,apps/server/src/server.ts:297; RFC 7692). - The mobile runtime uses the global
WebSocket(apps/mobile/src/lib/runtime.ts:24-32). On iOS that is React Native's SocketRocket, which does not offer permessage-deflate. On Android it is OkHttp, which does. - #4794 added a small Apple-only module backed by
URLSessionWebSocketTask, which offers the extension automatically. It was closed in "backlog sweep (wave 1)" with the reason "server permessage-deflate already shipped in #4705". Reopening or rebasing it may be the quickest fix.
Steps to reproduce
- Connect the iOS app and the Android or desktop app to the same server.
- Capture each client's
GET /wsupgrade request and the server's101response. - Compare
Sec-WebSocket-Extensions, and the wire size of the same push to each client.
Expected: iOS offers permessage-deflate and gets compressed frames. Actual: it offers no extensions, and every frame is uncompressed.
Version
iOS app build 122 (T3Code/122 CFNetwork/3860.700.2 Darwin/25.6.0). Server 0.1.1-nightly.20261018.3302. Code references are to main @ 9c161462c.
Environment
iOS 26, on Wi-Fi and cellular. Server on Linux x64, Node 26.8.2, behind a reverse proxy. Compared with the Windows desktop and Android apps on the same server and account.
Evidence
Upgrade requests captured on the server in the same hour. The iOS client was identified by its other requests from the same address, which carry the T3Code/122 user agent.
| 1 | client User-Agent on GET /ws offered server replied |
| 2 | iOS app (none) (no Sec-WebSocket-Extensions) (none) |
| 3 | Android app okhttp/5.4.0 permessage-deflate permessage-deflate |
| 4 | Windows desktop Electron permessage-deflate; client_max_window_bits permessage-deflate |
Over 24.5 hours, iOS had 72 WebSocket connections, none compressed. It received 365 MB in total, 360 MB of it as WebSocket frames. The desktop's 17 and Android's 6 were all compressed. Within the same minute, the same providerStatuses push was 558 KB to the iPhone and 141 KB to the desktop. At that ratio, the iPhone's 360 MB of WebSocket traffic would have been about 90–100 MB.
Most of that volume is the provider list (#21178, #21181). The 4× applies to every message, though, because thread snapshots, shell updates and turn events are all JSON.
Related issues
- #4705 (merged): permessage-deflate on the server.
- #4794 (closed, not merged): the iOS fix.
- #14264 (open): a triage comment there notes that SocketRocket also cannot take
allowsExpensiveNetworkAccessormultipathServiceType. AURLSessionWebSocketTasksocket would help there too. - #5178 (open): the experimental SwiftUI client already uses
URLSessionWebSocketTask.
Fix applied or workaround
None on the client. Staying on Wi-Fi avoids the mobile data cost, but not the extra server upload.
Filed by
Claude Code (claude-opus-5-5) via t3 triage, from the reporter's draft and packet captures.
Here's the device capture requested above. I connected both phones to the same server (0.1.2-nightly.20261021.3499, behind a reverse proxy) three minutes apart and captured the upgrades on the server. Only secrets and proxy headers are removed.
iOS (app 2.0.0 build 122, iOS 27; both connections looked like this):
| 1 | GET /ws?wsTicket=<redacted>&clientSurface=mobile&clientAppVersion=2.0.0&clientOs=iOS&clientOsMajorVersion=27&… HTTP/1.1 |
| 2 | Connection: Upgrade |
| 3 | Sec-Websocket-Key: <redacted> |
| 4 | Sec-Websocket-Version: 13 |
| 5 | Upgrade: websocket |
| 6 | Accept-Encoding: gzip |
| 7 | |
| 8 | HTTP/1.1 101 Switching Protocols |
| 9 | Upgrade: websocket |
| 10 | Connection: Upgrade |
| 11 | Sec-WebSocket-Accept: <redacted> |
Android (app 2.0.0, Android 16):
| 1 | GET /ws?wsTicket=<redacted>&clientSurface=mobile&clientAppVersion=2.0.0&clientOs=Android&clientOsMajorVersion=16&… HTTP/1.1 |
| 2 | User-Agent: okhttp/5.4.0 |
| 3 | Accept-Encoding: gzip |
| 4 | Connection: Upgrade |
| 5 | Sec-Websocket-Extensions: permessage-deflate |
| 6 | Sec-Websocket-Key: <redacted> |
| 7 | Sec-Websocket-Version: 13 |
| 8 | Upgrade: websocket |
| 9 | |
| 10 | HTTP/1.1 101 Switching Protocols |
| 11 | Upgrade: websocket |
| 12 | Connection: Upgrade |
| 13 | Sec-WebSocket-Accept: <redacted> |
| 14 | Sec-WebSocket-Extensions: permessage-deflate |
Android negotiates deflate through the same proxy, but iOS never offers it. This is the installed build, not current main. However, main's apps/mobile/src/lib/runtime.ts still uses Socket.layerWebSocketConstructorGlobal, and its only change since the report (c21700d412) doesn't touch the socket. Could #4794 be reopened or rebased?