Skip to content

Web UI fails to start through Tailscale Serve on macOS: the startup chunk burst overflows the server's listen queue #36409

Open

Description

@alexandercaley

What happened

A headless t3 serve (background service) on a Mac mini is reached remotely in Safari through Tailscale Serve (https://<host>.ts.net:10000 → http://127.0.0.1:3773). The page often fails to load and shows "T3 Code failed to start" with:

TypeError: Importing a module script failed.
Version: 0.1.9  Channel: stable  Surface: web
User agent: ... Version/27.2 Safari/605.1.15

Reloading sometimes works and sometimes doesn't. Opening the same server directly on 127.0.0.1:3773 always works. In practice, the web UI of a remote headless server can't be used reliably over Tailscale Serve.

Diagnosis

  • A fresh load requests about 300 JS chunks at once: main-*.js plus its static dependencies, out of 959 in client/assets.
  • Safari sends them over a single HTTP/2 connection. Tailscale Serve's Go reverse proxy opens a separate TCP connection to 127.0.0.1:3773 for each one.
  • The backend's listen queue holds only 128 connections (macOS kern.ipc.somaxconn = 128; netstat -Lan shows 0/0/128 *.3773). Part of the burst is refused at connect.
  • Tailscale logs bursts of http: proxy error: dial tcp 127.0.0.1:3773: connect: connection reset by peer (22 to 43 per load), plus some read: connection reset by peer. The refused requests never reach the T3 server, so they don't appear in server.trace.ndjson.
  • The browser gets a 502 for those chunks, so import("./main") in apps/web/src/bootstrap.ts rejects and goes straight to showBootError. There is no retry on this path. The vite:preloadError / reloadOnceForChunkLoadError guard lives in main.tsx, which never loaded.

Contributing factors on the T3 side:

  1. The size of the startup chunk fan-out.
  2. No retry when the boot import fails.
  3. T3's own --tailscale-serve sets up exactly this proxy, so it's a supported path.

Not fully explained: a few quick reloads failed without the server seeing a request for main-*.js at all (index-*.js revalidated with 304, then nothing). Safari may be reusing the earlier failure.

Steps to reproduce

  1. On macOS, run t3 serve on 127.0.0.1:3773.
  2. Expose it with tailscale serve --bg --https=10000 http://127.0.0.1:3773.
  3. From the server itself, fire about 300 parallel HTTP/2 requests for the bundled chunks through the Serve URL:
    cd <runtime>/versions/0.1.9/client/assets
    ls *.js | head -300 | sed 's#^#url = "https://<host>.ts.net:10000/assets/#;s#$#"\noutput = "/dev/null"#' > /tmp/urls.cfg
    curl -s --http2 --parallel --parallel-max 300 -K /tmp/urls.cfg -w '%{http_code}\n' | sort | uniq -c
    Result, three runs: 236/64, 219/81, 249/51 (200/502).
  4. Run the same 300 requests against http://127.0.0.1:3773 directly: all 300 return 200.
  5. In a browser, open the Serve URL and reload a few times. The boot error appears intermittently.

Version

Server 0.1.9 (stable), background service (launchd). The triage CLI was 0.1.2; it was not involved.

Environment

  • macOS 27.2.1, arm64 (Mac mini M4); server runs as a launchd service.
  • Client: Safari 27.2 on macOS over the tailnet.
  • Tailscale 1.104.5 (macOS app); Serve set up by hand, HTTPS port 10000 → http://127.0.0.1:3773.

Evidence

Server trace for Safari loads through Serve (index-*.js status → whether main-*.js followed):

13:31:12 200 index -> main OK   (Tailscale: 22 dial resets + 16 read resets at 13:31:12)
13:31:15 304 index -> NO main
13:31:16 304 index -> NO main
13:31:32 304 index -> NO main
13:31:33 304 index -> NO main
13:32:25 200 index -> main OK   (29 dial resets)
13:32:51 200 index -> main OK   (43 dial resets)
13:32:53 304 index -> NO main
13:34:29 304 index -> NO main   (boot error reported at 21:34:29Z)
13:38:14 304 index -> main OK   (8 dial resets + 20 read resets; this load worked)

Tailscale's unified log (process IPNExtension):

http: proxy error: dial tcp 127.0.0.1:3773: connect: connection reset by peer
http: proxy error: read tcp 127.0.0.1:54468->127.0.0.1:3773: read: connection reset by peer
http: proxy error: write tcp 127.0.0.1:54484->127.0.0.1:3773: write: broken pipe

All assets are served cache-control: no-cache, so every load revalidates the full chunk set.

Related issues

Fix applied or workaround

None applied. Possible directions:

  • Retry the boot import("./main") once or twice with a short backoff before showing the boot error.
  • Reduce the number of startup chunks, for example by merging the per-icon chunks.
  • Set a larger listen backlog on the server socket. macOS still caps it at kern.ipc.somaxconn, so sudo sysctl kern.ipc.somaxconn=1024 is a possible local workaround (untested).

Filed by

Claude Opus 5.5 (claude-opus-5-5) in Claude Code, via t3 triage.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions