toeverything/AFFiNE · warning · Error

Too many concurrent writings

Error message

Too many concurrent writings

What it means

Thrown by `PgUserspaceDocStorageAdapter.lockDocForUpdate` when `mutex.acquire('userspace:' + spaceId + ':' + docId)` fails to acquire a lock. This is a bare `new Error('Too many concurrent writings')` (NOT a `UserFriendlyError`), so the global exception filter wraps it into a generic `internal_server_error` with no dedicated `code`. It signals transient write contention on a single user's doc.

Source

Thrown at packages/backend/server/src/core/doc/adapters/userspace.ts:127

    };
  }

  protected async setDocSnapshot(snapshot: DocRecord) {
    // we always get lock before writing to user snapshot table,
    // so a simple upsert without testing on updatedAt is safe
    await this.models.userDoc.upsert({
      ...snapshot,
      blob: Buffer.from(snapshot.bin),
    });

    return true;
  }

  protected override async lockDocForUpdate(spaceId: string, docId: string) {
    const lock = await this.mutex.acquire(`userspace:${spaceId}:${docId}`);

    if (!lock) {
      throw new Error('Too many concurrent writings');
    }

    return lock;
  }
}

View on GitHub (pinned to 26c515e050)

Solutions

  1. Retry the write after a short backoff (the lock is released as soon as the in-flight writer finishes).
  2. Coalesce pending updates client-side so a single push carries them, reducing concurrent writers per doc.
  3. If self-hosting, raise the mutex capacity / lock pool size to match expected concurrency.
  4. Cap per-user concurrent writers in the client (queue, don't fan out).

Example fix

// before
await userspace.pushDocUpdates(userId, docId, updates, editorId);

// after
async function pushWithBackoff(updates, attempt = 0) {
  try {
    return await userspace.pushDocUpdates(userId, docId, updates, editorId);
  } catch (e) {
    if (e?.message === 'Too many concurrent writings' && attempt < 5) {
      await sleep(2 ** attempt * 50); // 50,100,200,400,800 ms
      return pushWithBackoff(updates, attempt + 1);
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Reduce concurrent writers per user+doc: queue updates instead of fanning out
async function pushCoalesced(userId: string, docId: string, updates: Uint8Array[]) {
  const queue = pendingQueues.open(`${userId}:${docId}`);
  queue.push(...updates);
  return queue.flush(); // serialized, single in-flight push per doc
}

Type guard

function isConcurrentWriteError(e: unknown): boolean {
  return e instanceof Error && /Too many concurrent writings/.test(e.message);
}

Try / catch

async function pushWithBackoff(updates: Uint8Array[], attempt = 0) {
  try {
    return await userspace.pushDocUpdates(userId, docId, updates, editorId);
  } catch (e) {
    if (isConcurrentWriteError(e) && attempt < 5) {
      await sleep(2 ** attempt * 50); // 50,100,200,400,800 ms
      return pushWithBackoff(updates, attempt + 1);
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: Many concurrent `pushDocUpdates` calls for the same user+doc at once — e.g., multiple collab tabs/devices, rapid batched syncs, or a retry storm — exhausting the per-key mutex pool before any writer releases.

Common situations: Same user syncing the same doc from several devices/tabs simultaneously; a client bug resending updates in a tight loop; the mutex pool size too small for the burst.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/f8cc8cac328854c5. Report an issue: GitHub.