{"record":{"id":"f8cc8cac328854c5","repo":"toeverything/AFFiNE","slug":"too-many-concurrent-writings","errorCode":null,"errorMessage":"Too many concurrent writings","messagePattern":"Too many concurrent writings","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/backend/server/src/core/doc/adapters/userspace.ts","lineNumber":127,"sourceCode":"    };\n  }\n\n  protected async setDocSnapshot(snapshot: DocRecord) {\n    // we always get lock before writing to user snapshot table,\n    // so a simple upsert without testing on updatedAt is safe\n    await this.models.userDoc.upsert({\n      ...snapshot,\n      blob: Buffer.from(snapshot.bin),\n    });\n\n    return true;\n  }\n\n  protected override async lockDocForUpdate(spaceId: string, docId: string) {\n    const lock = await this.mutex.acquire(`userspace:${spaceId}:${docId}`);\n\n    if (!lock) {\n      throw new Error('Too many concurrent writings');\n    }\n\n    return lock;\n  }\n}\n","sourceCodeStart":109,"sourceCodeEnd":133,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/doc/adapters/userspace.ts#L109-L133","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nawait userspace.pushDocUpdates(userId, docId, updates, editorId);\n\n// after\nasync function pushWithBackoff(updates, attempt = 0) {\n  try {\n    return await userspace.pushDocUpdates(userId, docId, updates, editorId);\n  } catch (e) {\n    if (e?.message === 'Too many concurrent writings' && attempt < 5) {\n      await sleep(2 ** attempt * 50); // 50,100,200,400,800 ms\n      return pushWithBackoff(updates, attempt + 1);\n    }\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"// Reduce concurrent writers per user+doc: queue updates instead of fanning out\nasync function pushCoalesced(userId: string, docId: string, updates: Uint8Array[]) {\n  const queue = pendingQueues.open(`${userId}:${docId}`);\n  queue.push(...updates);\n  return queue.flush(); // serialized, single in-flight push per doc\n}","typeGuard":"function isConcurrentWriteError(e: unknown): boolean {\n  return e instanceof Error && /Too many concurrent writings/.test(e.message);\n}","tryCatchPattern":"async function pushWithBackoff(updates: Uint8Array[], attempt = 0) {\n  try {\n    return await userspace.pushDocUpdates(userId, docId, updates, editorId);\n  } catch (e) {\n    if (isConcurrentWriteError(e) && attempt < 5) {\n      await sleep(2 ** attempt * 50); // 50,100,200,400,800 ms\n      return pushWithBackoff(updates, attempt + 1);\n    }\n    throw e;\n  }\n}","preventionTips":["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)."],"tags":["doc-storage","userspace","concurrency","mutex","lock","retry"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}