Repository navigation
Every client connect waits ~6.5 s on sequential, uncached Tailscale discovery when tailscale status fails #26775
Description
What happened
The Android app is slow to connect and sync over Tailscale, both on cold start and when it comes back from the background. The server is the desktop app on a Mac, reached through Tailscale Serve.
Diagnosis
Every subscribeServerConfig runs ws.loadServerConfig (apps/server/src/ws.ts). It awaits its discoveries one after another, and none of them is cached between connects:
RemoteOpenTargets.resolveTargets()runstailscale status --json. That's capped at 3.5 s (TAILSCALE_STATUS_TIMEOUT,packages/tailscale/src/tailscale.ts).DirectEndpoints.resolve()runstailscale status --jsonagain because Tailscale Serve is enabled. That's capped at 3 s (TAILSCALE_ENDPOINT_TIMEOUT,apps/server/src/environment/DirectEndpoints.ts).
The client's session.ready waits for that first config snapshot (packages/client-runtime/src/rpc/session.ts), and the shell and thread subscriptions wait for session.ready. So whenever tailscale status fails slowly, every connect and reconnect waits about 6.5 s before anything syncs. The client's connection budget is 15 s, and this uses almost half of it.
This is the part of #14517 the maintainers left open ("Initial config still awaits sequential editor, file-manager, and remote-target probes; the latter probes remain uncached"). Below is a concrete, reproducible trigger and its cost.
What makes tailscale status fail here: tailscaled runs in userspace-networking mode with a non-default --socket, and no Tailscale app is installed. The CLI finds the daemon only through --socket; the macOS default is /var/run/tailscaled.socket. It reads no environment variable or config file for this (TS_SOCKET is used only by containerboot). So T3's bare tailscale status fails after about 5 s, and T3 has no option for the socket or the CLI path.
Steps to reproduce
- On macOS, with no Tailscale app installed, run
tailscaled --tun=userspace-networking --socket=$HOME/.tailscale-userspace/tailscaled.sock. Check thattailscale statuswith no--socketfails after about 5 s (dial unix /var/run/tailscaled.socket: connect: no such file or directory). - Run the T3 Code desktop app with Tailscale Serve enabled.
- Connect any client: the mobile app over Serve, or a bearer-token client on
127.0.0.1:3773. For the latter, mint a ticket withPOST /api/auth/websocket-ticket/rpc, open/ws, and sendsubscribeServerConfig. - Every connect waits about 6.5 s for the first config chunk.
server.trace.ndjsonshowsws.loadServerConfigat about 6510–6550 ms, withtailscale.readTailscaleOutputfailing withTailscaleCommandTimeoutError.
Version
Server 0.1.4-nightly.20261028.3824 (desktop app). Android app 2.0.0.
Environment
macOS 27.0.1 (arm64), T3 Code desktop nightly, Tailscale 1.104.2 (Homebrew, userspace tailscaled), Android 17 (Pixel 10 Pro).
Evidence
# The CLI alone, same machine
$ time tailscale status --json # 5.1 s, fails: dial unix /var/run/tailscaled.socket
$ time tailscale --socket=<socket> status --json # 0.03 s
$ time TS_SOCKET=<socket> tailscale status --json # 5.1 s, same failure
# server.trace.ndjson, one connect from the Android app (okhttp/5.4.0, clientSurface=mobile)
ws.loadServerConfig 6546.6 ms
RemoteOpenTargets.resolveTargets 3509.4 ms
tailscale.readTailscaleOutput 3508.3 ms TailscaleCommandTimeoutError: tailscale status timed out after 3500ms.
DirectEndpoints.resolve 3005.5 ms
tailscale.readTailscaleOutput 3004.9 ms Interrupted
# Controlled A/B on the same running server, loopback client,
# ticket -> /ws open -> first subscribeServerConfig chunk, 5 runs each
bare `tailscale` on PATH (fails): 6551, 6551, 6553, 6554, 6554 ms (server-side loadServerConfig 6511-6515 ms)
`tailscale` wrapper adding --socket: 91, 91, 92, 100, 105 ms (server-side 63-78 ms)
# Android app connects, ws.loadServerConfig
before the wrapper: 6542, 6547 ms
after the wrapper: 83, 84, 94, 96, 430, 457, 468, 503 ms
# The ~350 ms extra on some connects is the Serve self-probe failing DNS
# (this userspace host can't resolve *.ts.net).Related issues
#14517 (sequential and uncached discovery; this reports the part left open there). #14519 (open PR: concurrent discovery plus stale-while-revalidate for editors and remote targets, but DirectEndpoints stays uncached, so each connect here would still wait about 3 s). #26103 (open PR for #11929: caches Tailscale reads on macOS, failures for 60 s, so this setup would still wait about 3.5 s on the first connect after each expiry). #15919 (macOS: the CLI can't be found on the app's PATH, another way tailscale status fails). #24909 and #16549 (iOS reconnect loop; #24909 saw the same 3.5 s timeouts but tracks the reconnect loop).
Fix applied or workaround
A wrapper earlier on the server's PATH runs exec /opt/homebrew/bin/tailscale --socket="$HOME/.tailscale-userspace/tailscaled.sock" "$@". With it, config load takes 63–78 ms on loopback and 83–503 ms for the phone.
Possible fixes upstream:
- Don't make connects wait on the Tailscale discovery: run it concurrently with the other discovery steps, and cache it across connects, including on failure and including
DirectEndpoints. - Separately, if it's in scope: an option for the Tailscale CLI path or socket (CONTRIBUTING's "executable path for an already-supported hosting CLI" route), so a userspace
tailscaledcan be reached at all.
Filed by
claude (Claude Opus 5.5) in a T3 Code thread, following the t3 triage playbook
Activity