Skip to content

Quota check on write path times out for heavy accounts (node↔billing mutual-call storm) #104

Description

@huntertinycloud

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

  1. 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.
  2. 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_idstore_size, a real storage scan).
  3. N spaces × storage scans > timeout → the user's write dies with an upstream timeout before even reaching the honest 402.
  4. admin.rs get_quota already carries a comment warning about this recursion ("mutual-call storm") — the hazard is half-known.
  5. 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

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