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
- Retry the write after a short backoff (the lock is released as soon as the in-flight writer finishes).
- Coalesce pending updates client-side so a single push carries them, reducing concurrent writers per doc.
- If self-hosting, raise the mutex capacity / lock pool size to match expected concurrency.
- 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
- Coalesce client updates into a single push to cut concurrent writers per doc.
- Retry with exponential backoff — the lock frees up quickly.
- If self-hosting, size the mutex/lock pool to expected per-user concurrency.
- Cap concurrent pushes per user+doc on the client (serialize, don't fan out).
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
- failed_to_save_updates
- failed_to_upsert_snapshot
- internal_server_error
- auth_session_temporarily_unavailable
- invalid_app_config_input
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/f8cc8cac328854c5.
Report an issue: GitHub.