Why the WebSocket took minutes The HAR has three socket entries: two that ended in error and one that succeeded at 15:56:05 — 2.5 minutes after the history REST query settled (~15:53:34, which is when wsUrl ungates in conversation-websocket-context.tsx). The relative error timestamps show the first /sockets/events/... attempt was opened right on time but its handshake hung for ~2.5 minutes before erroring; the retry 3 seconds later (the fixed delay in use-websocket.ts) connected instantly. Two codebase problems compound here: No handshake timeout anywhere. Neither use-websocket.ts nor use-bash-command-runner.ts bounds the time between new WebSocket(url) and onopen. If the server stalls the 101 upgrade (which it plausibly did — server-side wait times on ordinary requests were 1–3.5 s while it was busy streaming 1.35 MB search dumps), the browser will wait indefinitely. Chrome serializes WebSocket handshakes per endpoint (the WebSocket endpoint lock): only one WS handshake may be in flight to a given host at a time. The app opens two sockets to the same host — /sockets/bash-events (opened eagerly by useBashCommandRunner via use-local-git-info) and /sockets/events/{id}. The HAR error timestamps are 75 s apart: one hung handshake held the endpoint lock and blocked the other socket until it died. So a stall on the bash socket delays the events socket — the one the chat actually needs. And the reason the server stalled in the first place feeds back into the polling: the 10-second conversations/search poll takes 7–11 s per response on this connection (1.35 MB, uncompressed, HTTP/1.1), so the client is effectively downloading the full conversation list continuously, keeping both the network and the agent-server busy. What to fix WebSocket connection (the "minutes to connect" bug): Add a handshake timeout to use-websocket.ts and use-bash-command-runner.ts — if the socket isn't OPEN within ~10 s, close() it and retry. Closing releases Chrome's per-endpoint handshake lock, so a hung attempt can't block the other socket for minutes. This is the single highest-impact fix. Add exponential backoff + jitter to the reconnect (currently a fixed 3 s), so retries don't hammer an already-struggling server. Stop opening /sockets/bash-events eagerly. use-local-git-info keeps a persistent bash socket just to re-probe git remote/branch every 10 s. Open it lazily on first command and prioritize the events socket connecting first (or delay bash-events until the events socket is OPEN) so the chat socket never queues behind it. Duplicate / repetitive requests: usePaginatedConversations (refetchInterval: 10_000) is the worst offender — 92% of bytes transferred. The sidebar list only needs title/status/updated_at, but /api/conversations/search returns full agent config, stats, client_tools, and secret_registry per item (~68 KB per conversation). Frontend-side: lengthen the interval substantially (30–60 s), and skip scheduling a new tick while the previous fetch is still in flight (on slow links the fetch outlives the interval, making it a continuous download). Better: push execution-status/title updates over the already-open events socket and drop the poll to a slow safety-net refresh. (Server-side, a slim list projection is the real fix, but that's the agent-server.) useBackendsHealth fires GET /api/settings + GET /server_info every 10 s per backend — 51 of the 310 requests. The QueryCache.onSuccess handler already calls recordBackendSuccess() for every successful app query; use that as the primary liveness signal and only actively probe when there's been no successful traffic for N seconds, or when the last verdict was a failure. At minimum raise REFRESH_INTERVAL_MS to 30–60 s and drop the getSettings pre-check after the first success. Redundant conversation fetches: useActiveConversation polls GET /api/conversations?ids={id} every 30 s and the deployed build polls GET /api/conversations/{id} every 30 s via useConversationMetrics (33 KB each, both containing the same metrics/stats) — while the WS stats events already stream the same numbers into useMetricsStore. The metrics poll should be gated on the metrics modal being open (as metrics-modal.tsx intends) and can drop its refetchInterval entirely once WS is connected; the active-conversation slow poll can also back off while the socket is OPEN, since execution-status changes arrive as events. Set React Query global defaults. createAgentServerQueryClient() sets no defaultOptions, so every query without an explicit staleTime uses staleTime: 0 + refetchOnWindowFocus: true. That's what the request bursts at 15:53:34 / 15:54:23 / 15:55:14 / 15:55:59 / 15:56:52 are: each tab focus refires search_subdirs, workspace/README.md, api/workspaces, bash probes, etc. A default staleTime of ~30 s and refetchOnWindowFocus: false (or "always" only where freshness genuinely matters) removes most of these. Duplicate git/changes + git/commits pairs (two identical requests ~30 ms apart at 15:56:08 and 15:57:06): the query keys in useUnifiedGetGitChanges/useUnifiedGitCommits include conversationUrl/sessionApiKey, so a key transitions (undefined → resolved) or two tab consumers mounting with refetchOnMount: "always" produce back-to-back identical fetches. Keying on conversationId only (the URL/key are derivable) and relying on staleTime instead of refetchOnMount: "always" dedupes them. Startup double-fetches: GET /api/settings twice in parallel at t=0 (the app's useSettings and the health probe's probeBackend both fetch it) and POST /api/skills twice. Sharing the settings query between the two callers (or having the probe read the query cache first) removes the duplication. Priority order: #1 (handshake timeout) and #3 (don't let bash-events block the chat socket) fix the visible "minutes to connect" symptom directly; #4 and #5 remove ~95% of the repetitive traffic that starved the connection and loaded the server enough to hang the handshake in the first place; #7 kills the focus-refetch bursts across the board. Want me to implement these? I'd start with the websocket handshake timeout + backoff and the polling-interval/health-probe changes, which are all small, well-contained diffs.