Decision record
ADR-0001: Yjs collaboration service on Cloud Run
ADR-0001: Yjs collaboration service on Cloud Run
Status
Accepted — 2026-08-27 (spike results below).
Context
Strategy §13 calls for authenticated Yjs rooms over WebSockets. Cloud Run
supports WebSockets but has properties that could disqualify it: instances are
ephemeral, requests have a max timeout (configurable to 60 min), multiple
instances would split a room’s clients unless affinity keeps them together,
and scale-in can kill an instance holding live rooms. PLAN Phase 3 required a
load spike before accepting the platform (scripts/collab-spike.mjs).
Spike results
Local (M-series MacBook, emulator Firestore, one collab process):
- 500 concurrent sockets across 20 rooms: 500/500 connected, connect+sync p50 209ms / p95 277ms, update propagation p50 9ms / p95 13ms.
Production (Cloud Run us-central1, 512Mi/1cpu, min-instances 1, session affinity, 60-min request timeout):
- 25 concurrent sockets in one room over TLS: 25/25 connected, connect+sync p95 1.28s (TLS + cold path), update propagation p50 74ms / p95 87ms.
Decision
Run collab on Cloud Run with: --session-affinity (keeps one browser’s
reconnects on the same instance), --min-instances 1 (avoids cold starts on
the realtime path), --timeout 3600 (max socket lifetime; the y-websocket
client reconnects transparently at timeout), and --max-instances 3 for now.
Room-splitting across instances is tolerated, not prevented: affinity makes
splits rare at current scale, and a split room still converges because every
checkpoint writes through Cloud Storage (collab.yupdate) and clients resync
on reconnect. Checkpointing on 30s intervals + last-disconnect + SIGTERM
bounds data loss when an instance is reclaimed to ≤30s of edits, which the
autosave revision path narrows further.
Consequences
- No sticky external state service (no Redis — CLAUDE.md) is needed at MVP scale; the Y.Doc lives in instance memory between checkpoints.
- Above ~3 instances or with heavy multi-instance room splits, move to a room-sharded routing layer (hash pageId → instance via an internal LB or a Pub/Sub relay). Revisit when concurrent editors per workspace grow past hundreds.
- The 60-minute socket ceiling is acceptable: y-websocket reconnects and Yjs state vectors make resync cheap.
References
- WebSockets on Cloud Run — the platform limits weighed here
- Yjs — the CRDT the rooms are built on