toeverything/AFFiNE · critical · FailedToSaveUpdates
failed_to_save_updates
failed_to_save_updates
Error message
Failed to store doc updates.
What it means
PgWorkspaceDocStorageAdapter.pushDocUpdates wraps batched inserts of Yjs binary updates (models.doc.createUpdates in batches of 10 inside a retryable() block) plus enqueueing of the doc.mergePendingDocUpdates job. Any database error that survives the retry budget is logged, increments the doc_update_insert_failed metric, and is rethrown as FailedToSaveUpdates (internal_server_error / failed_to_save_updates). This is a server-side persistence failure, not a client-input problem.
Solutions
- Read the preceding server log line 'Failed to insert doc updates' — it contains the underlying database error that this wrapper hides
- Check database health, connectivity, and that all migrations are applied (prisma migrate deploy)
- Retry the push after the DB issue is fixed — client-side Yjs updates are still buffered locally and can be re-pushed
- If the FK points at a deleted workspace, discard the local updates for that workspace or recreate the workspace before pushing
Example fix
// before
await docSync.push(workspaceId, docId, updates);
// after
try {
await docSync.push(workspaceId, docId, updates);
} catch (e) {
if (e.extensions?.code === 'failed_to_save_updates') {
// server logged root cause; keep local updates and retry later
return scheduleRetry(updates);
}
throw e;
} Defensive patterns
Strategy: retry
Validate before calling
// pre-flight: confirm the workspace still exists so the FK target is valid
const exists = await db.workspace.findUnique({ where: { id: workspaceId } });
if (!exists) return discardLocalUpdates(workspaceId, docId); Type guard
function isFailedToSaveUpdates(e: unknown): boolean {
return (e as { extensions?: { code?: string } }).extensions?.code === 'failed_to_save_updates';
} Try / catch
try {
await pushUpdates(workspaceId, docId, updates);
} catch (e) {
if (isFailedToSaveUpdates(e)) {
keepLocalUpdatesForRetry(updates); // updates are still client-side; retry with backoff
return scheduleRetry();
}
throw e;
} Prevention
- Monitor the doc_update_insert_failed metric and alert on it
- Keep prisma migrations in lockstep with the deployed schema
- Never delete a workspace while sync clients are still pushing — quiesce first
When it happens
Trigger: Pushing doc updates while Postgres is unreachable or the connection pool is exhausted; a foreign-key/unique violation when the workspace was deleted (deleteSpace) while a client was still pushing updates; deadlocks or serialization failures exceeding the retry budget; schema drift after a missed migration.
Common situations: Self-hosted deployments with an under-provisioned database, disk-full events, managed DB failovers, or a deployment where prisma migrations were not applied; dev environments running an old branch against a new schema.
Related errors
- failed_to_upsert_snapshot
- A curve must have at least three points.
- Bad segment
- copilot_embedding_unavailable
- doc_history_not_found
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/eebe19001c844ae2.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/doc/adapters/workspace.ts:179
: await this.runtime.appendWorkspaceDocUpdatesV1({
...input,
...contract,
});
done += batch.length;
}
});
if (isNewDoc) {
await this.event.emitDetachedAsync('doc.created', {
workspaceId,
docId,
editor: editorId,
});
}
} catch (e) {
this.logger.error('Failed to insert doc updates', e);
metrics.doc.counter('doc_update_insert_failed').add(1);
throw new FailedToSaveUpdates();
}
return timestamp;
}
protected async getDocUpdates(workspaceId: string, docId: string) {
const rows = await this.models.doc.findUpdates(workspaceId, docId);
return rows.map(row => ({
bin: row.blob,
timestamp: row.timestamp,
editor: row.editorId,
}));
}
async deleteDoc(_workspaceId: string, _docId: string) {
return;
}
View on GitHub (pinned to 2af30773ae)