Symptom
Users with large/multi-space accounts get SQL batch failed: upstream request timed out on ordinary writes (observed on prod via Listen's schema-ensure for a ~78.5MB space, 2026-07-09). Small accounts are unaffected. Because writes fail, data loss is silent for webhook-driven ingestion (e.g. Listen transcripts).
Trace
- Write arrives → node consults
QuotaCache.get_limit() (tinycloud-node-server/src/quota.rs): cache miss → synchronous HTTP GET {quota_url}/api/quota/{space_id} with a 10s timeout, inside the user's write request.
- Billing sidecar (
billing/src/index.ts, GET /api/quota/:space_id) computes effective limit = subscription budget − live usage of all other spaces the DID owns, fetching each space's usage by calling back to the node (GET /admin/quota/:space_id → store_size, a real storage scan).
- N spaces × storage scans > timeout → the user's write dies with an upstream timeout before even reaching the honest 402.
admin.rs get_quota already carries a comment warning about this recursion ("mutual-call storm") — the hazard is half-known.
- Aggravator:
QuotaCache.overrides is an in-memory HashMap — every node restart is a cold start that re-triggers the storm, and admin overrides silently evaporate.
Root cause
The quota contract requires live cross-space usage aggregation at read time. That makes slowness structural.
Proposed fixes
- billing: precompute per-space effective limits into D1; recompute asynchronously on Stripe webhooks and periodic/pushed usage reports.
/api/quota becomes a plain row read.
- node: cache
store_size/quota answers with a TTL; move quota refresh off the write path (background refresh, stale-while-revalidate).
- policy: on quota-service timeout, fail open with last-known limit (or env default) instead of failing the write — a billing hiccup should never deny storage writes.
- persist admin quota overrides (or document that they are ephemeral).
Severity
High — hits the heaviest/most engaged accounts first, and the failure mode upstream of the 402 is silent write loss.
Found during the Listen UX/freshness audit (branch ux/library-navigation); quota UX on the Listen side is handled separately (402 'Storage full' state shipped; P8 usage-meter planned).
🤖 Generated with Claude Code
Symptom
Users with large/multi-space accounts get
SQL batch failed: upstream request timed outon ordinary writes (observed on prod via Listen's schema-ensure for a ~78.5MB space, 2026-07-09). Small accounts are unaffected. Because writes fail, data loss is silent for webhook-driven ingestion (e.g. Listen transcripts).Trace
QuotaCache.get_limit()(tinycloud-node-server/src/quota.rs): cache miss → synchronous HTTP GET{quota_url}/api/quota/{space_id}with a 10s timeout, inside the user's write request.billing/src/index.ts,GET /api/quota/:space_id) computes effective limit = subscription budget − live usage of all other spaces the DID owns, fetching each space's usage by calling back to the node (GET /admin/quota/:space_id→store_size, a real storage scan).admin.rsget_quotaalready carries a comment warning about this recursion ("mutual-call storm") — the hazard is half-known.QuotaCache.overridesis an in-memory HashMap — every node restart is a cold start that re-triggers the storm, and admin overrides silently evaporate.Root cause
The quota contract requires live cross-space usage aggregation at read time. That makes slowness structural.
Proposed fixes
/api/quotabecomes a plain row read.store_size/quota answers with a TTL; move quota refresh off the write path (background refresh, stale-while-revalidate).Severity
High — hits the heaviest/most engaged accounts first, and the failure mode upstream of the 402 is silent write loss.
Found during the Listen UX/freshness audit (branch
ux/library-navigation); quota UX on the Listen side is handled separately (402 'Storage full' state shipped; P8 usage-meter planned).🤖 Generated with Claude Code