Skip to content

Every client connect waits ~6.5 s on sequential, uncached Tailscale discovery when tailscale status fails #26775

Open

Description

@rcnsh

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:

  1. RemoteOpenTargets.resolveTargets() runs tailscale status --json. That's capped at 3.5 s (TAILSCALE_STATUS_TIMEOUT, packages/tailscale/src/tailscale.ts).
  2. DirectEndpoints.resolve() runs tailscale status --json again 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

  1. On macOS, with no Tailscale app installed, run tailscaled --tun=userspace-networking --socket=$HOME/.tailscale-userspace/tailscaled.sock. Check that tailscale status with no --socket fails after about 5 s (dial unix /var/run/tailscaled.socket: connect: no such file or directory).
  2. Run the T3 Code desktop app with Tailscale Serve enabled.
  3. 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 with POST /api/auth/websocket-ticket/rpc, open /ws, and send subscribeServerConfig.
  4. Every connect waits about 6.5 s for the first config chunk. server.trace.ndjson shows ws.loadServerConfig at about 6510–6550 ms, with tailscale.readTailscaleOutput failing with TailscaleCommandTimeoutError.

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 tailscaled can be reached at all.

Filed by

claude (Claude Opus 5.5) in a T3 Code thread, following the t3 triage playbook

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions