[BUG] /zero/* WebSocket connections drop ~30s in, Documents panel never loads (self-hosted)
Bug Description
Every /zero/* WebSocket connection is closed abruptly by zero-cache roughly
30s after the handshake, in an infinite reconnect loop. The immediate effect
is that any UI backed by Zero (Documents list, notifications, etc.) never
finishes loading and stays stuck on its skeleton state, even though the
REST API works fine and documents are indexed successfully in the background.
Deployment Type
- SurfSense Cloud (hosted version)
- Self-hosted version
Steps to Reproduce
This reproduces with zero application state — no login, no cookies, no real client — so it's not specific to any account/workspace/document:
curl -s -i -N --http1.1 --max-time 40 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
"https://<your-domain>/zero/sync/v51/connect?clientID=test&clientGroupID=test&userID=test&baseCookie=&ts=0&lmid=0&wsid=test"Response: 101 Switching Protocols, then the connection sits open for
exactly ~30 seconds and delivers this payload before the socket dies:
Error: Unexpected undefined valueIn the real app: log in, open the Documents panel — it shows the skeleton
loading state and never resolves. DevTools console shows a WebSocket
reconnect loop against /zero/sync/v51/connect with ClientError: WebSocket connection closed abruptly, repeating with increasing backoff
forever.
Expected Behavior
The /zero/* WebSocket connection stays open and the Documents panel (and
other Zero-synced views) load normally.
Actual Behavior
zero-cache closes every connection ~30s in. Debug-level logs
(ZERO_LOG_LEVEL=debug) show the actual failure happens almost immediately
after initConnection, not at the 30s mark — the 30s is just ws's default
closeTimeout (node_modules/ws/lib/constants.js: CLOSE_TIMEOUT = 30000)
elapsing while it waits for a clean close handshake that never happens,
before hard-terminating the socket (hence code: 1006, wasClean: false
on the client side).
Relevant debug trace (single connection, clientID=onavlbmb2jcbfahafk):
{"level":"DEBUG",...,"message":"acquired lock for cvr"}
{"level":"DEBUG",...,"cmd":"initConnection","message":"mismatched wsID B-5e8huSLCsTscTm-sCL0"}
{"level":"DEBUG",...,"message":"No auth maintenance wakeup scheduled"}
{"level":"WARN","worker":"dispatcher","message":"closing connection to client with error Error: Unexpected undefined value"}
{"level":"INFO",...,"message":"closing clientGroupID=..."}
{"level":"WARN",...,"errorBody":{"kind":"Internal","message":"shut down before initialization completed","origin":"zeroCache"},"name":"ProtocolError",...}Traced Error: Unexpected undefined value to shared/src/must.js
(if (v == null) throw new Error(msg ?? \Unexpected ${v} value`)), and the close path to closeWithErrorinzero-cache/src/types/ws.js, called from the dispatcher's handoff error path in zero-cache/src/server/worker-dispatcher.js`.
Wasn't able to pin the exact call site further without unminified source —
happy to dig further if useful, or if there's a source-mapped build I can
point at instead.
Note: the mismatched wsID line in view-syncer.js (client?.wsID !== wsID → debug-log + early return) looks like a benign, expected no-op on
its own — flagging it only because it's the last view-syncer-side log line
before the dispatcher's crash, in case it's a useful breadcrumb.
Things I ruled out (self-hosted repro, all reproduce identically)
- Reverse proxy: replaced bundled Caddy with nginx-proxy-manager,
replicating the exact
/zero/context→ backend,/zero/*→ zero-cache routing fromdocker/proxy/Caddyfile. Bypassed the proxy entirely (curl straight to thezero-cachecontainer on the Docker network) — identical ~30s failure, so this isn't a proxy/header issue. ZERO_NUM_SYNC_WORKERS: reproduces at both4(default) and1.- Zero-cache version: reproduces identically on
1.6.0(the pin indocker/docker-compose.yml),1.6.2, andlatest(=1.9.0as of this writing, confirmed via Docker Hub digest match) — not a regression that got fixed upstream. - Stale/desynced Postgres state:
docker compose down -v(wipedpostgres_dataand every other named volume), freshalembic upgrade headon an empty database, brand-new user registration — same failure from the very first connection attempt.
Environment Information
- Browser: Chrome (latest)
- Operating System: self-hosted server: Ubuntu 24.04 (Docker host); client OS irrelevant, reproduces via bare curl
- SurfSense Version:
ghcr.io/modsetter/surfsense-backend:latest/surfsense-web:latest(pulled 2026-09-04)
Additional Environment Details (for Self-hosted only)
- Database:
pgvector/pgvector:pg17, fresh install,wal_level=logicalper bundledpostgresql.conf - Deployment Method: Docker Compose, adapted from
docker/docker-compose.yml— services/env otherwise unchanged (ETL_SERVICE=DOCLING,EMBEDDING_MODEL=litellm://text-embedding-3-small,AUTH_TYPE=LOCAL,SANDBOX_ENABLED=FALSE) - Only structural deviations from the upstream compose: nginx-proxy-manager instead of bundled Caddy (see above — proven not the cause), and
opensandbox-server/sandbox-imageomitted entirely (unrelated services, no dependency on zero-cache)
Additional Context
This blocks real usage, not just a console warning — the Documents panel
(and presumably notifications, chat live-updates, and anything else backed
by Zero per zero-sync.mdx) never leaves its loading skeleton, since it's
sync-only with no REST fallback visible in the UI. Document upload/indexing
via Celery and the REST API works fine in the background; it's specifically
the Zero-synced read paths that never resolve.
Happy to run more diagnostics (e.g. capture the exact WS frames sent by the real browser client, or bisect zero-cache versions further) — this repro is fully scripted so turnaround is fast.
Checklist
- I have searched existing issues to ensure this is not a duplicate
- I have provided all the required information above
- I have added appropriate labels (bug, deployment type)
Source: MODSetter/SurfSense