Additional report and server traces: #24909, iOS 2.1.0 (164), server nightly 20261023.3579. Reconnects continue after fixing the host tailscale CLI access: configuration loads in 0.4–1.4 s, but sockets still last 101 s, 71 s and 0 s, with overlapping old/new sockets. Thread snapshots take 0.1–0.4 s; opened threads can remain on Syncing.
[Bug]: iPad (iOS app and Safari/Chrome web) reconnect-loops on a remote environment although the server sends the first config snapshot in ~250 ms#16549
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
iOS app / web client on iPadOS (shared client runtime, I think)
Steps to reproduce
- Run the T3 Code background service (0.0.45) on a Linux desktop, bound to
127.0.0.1:3773, exposed inside a tailnet withtailscale serve(HTTPS 443 →http://127.0.0.1:3773, tailnet only, no Funnel). - Pair an iPad (iPadOS 26.6) with the iOS app via
t3 pair --tailscale. - Pair a MacBook (macOS desktop app) the same way, on the same Wi-Fi network.
- Leave both connected.
Expected behavior
The iPad stays connected, as the Mac does.
Actual behavior
iOS app (build 106): the environment cycles between connected and reconnecting. From the server trace, each attempt goes:
/.well-known/t3/environment200 →POST /api/auth/websocket-ticket200 →authenticateWebSocketUpgradesucceeds →/wsupgrade.ws.rpc.subscribeServerConfig(andorchestration.subscribeShell) start.- About 14.9 s later,
subscribeServerConfigendsInterruptedwithClientAbort. - The
/wsrequest closes about 45 s after it opened, and the client retries.
This repeated for over an hour (dozens of attempts). A few connections lasted longer, the longest about 5 min, then the loop resumed.
The ~15 s abort looks like CONNECTION_ESTABLISHMENT_TIMEOUT from #14516. Unlike #14516, the server is not slow. In the same traces, the snapshot's discovery work finishes in well under a second:
externalLauncher.resolveAvailableEditors: max 287 msresolveUsableFileManagerCommand: ~110 ms- no other span on the connect path above ~1 s
So the server appears to produce the snapshot in roughly 250 ms, but the iPad client still hits its 15 s budget.
Network ruled out: the loop happens both through the Tailscale DERP relay (70–500 ms) and over a direct path (~30 ms, tailscale ping). Over the same tailscale serve endpoint, on the same Wi-Fi, the macOS desktop app (and Chrome on macOS earlier) stays connected without drops.
Web client on the same iPad:
- Chrome (iOS, CriOS 154):
/pairloads and all assets 200.POST /api/auth/browser-session200 consumes the pairing token, and the session is created. But the page stays on the T3 loading screen and never requests a websocket ticket or opens/ws.t3 auth session listshows that session withlast connected: never. Reloading/pairthen re-posts the consumed token, which gets a 401 (UnknownBootstrapCredentialError). - Safari (iPadOS 26.6, desktop-class UA):
/pair, the manifest and the icon load. No/api/auth/sessionor/api/auth/browser-sessionrequest follows, so the pairing token is never redeemed. It stays on the loading screen.
Impact
Major degradation or frequent failure. The iPad can't be used as a client.
Version or commit
- Server: t3 0.0.45 (background service, Linux x64)
- iOS app: T3Code/106 (CFNetwork/3860.700.1, Darwin/25.6.0)
- Working comparison: macOS desktop app (Electron), same server
Environment
- Server: Ubuntu 24.04.3 LTS, kernel 6.8, Node via nvm (service PATH resolves
claudefine) - Client: iPad Air, iPadOS 26.6, Low Power Mode off
- Transport:
tailscale serveHTTPS proxy, Tailscale 1.102.5 on the server
Logs or stack traces
Server trace (server.trace.ndjson), one representative iOS app attempt:
| 1 | 19:13:16 http.server GET /ws 44885ms Success (closed) |
| 2 | 19:13:16 ws.rpc.subscribeServerConfig 14868ms Interrupted: InterruptError ... at ClientAbort |
| 3 | ├ externalLauncher.resolveAvailableEditors 227ms |
| 4 | └ externalLauncher.resolveUsableFileManagerCommand 108ms |
| 5 | 19:13:48 (next attempt, same shape) |
Chrome on iPad:
| 1 | 19:22:43 POST /api/auth/browser-session 200 (PairingGrantStore.consume Success) |
| 2 | 19:22:43 GET /api/auth/session 200 |
| 3 | 19:22:47 GET /pair 200 (repeats; no /api/auth/websocket-ticket, no /ws) |
| 4 | 19:23:32 POST /api/auth/browser-session 401 UnknownBootstrapCredentialError |
I don't have client-side traces from the iPad. Happy to collect them if you can point me at how.
Workaround
None on the iPad. The macOS desktop app works.