toeverything/AFFiNE · error · FailedToSaveUpdates

failed_to_save_updates

failed_to_save_updates

Error message

Failed to store doc updates.

What it means

Thrown by `PgWorkspaceDocStorageAdapter.pushDocUpdates` when the `retryable` batch insert of doc updates throws — every retriable attempt inside the try block failed. The original DB error is logged and a metric `doc_update_insert_failed` is incremented, then `FailedToSaveUpdates` (category `internal_server_error`) is thrown so the raw DB error is not leaked.

Source

Thrown at packages/backend/server/src/core/doc/adapters/workspace.ts:125

              priority: 100,
            }
          );
          turn++;
          done += batch.length;
        }
      });

      if (isNewDoc) {
        this.event.emitDetached('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 26c515e050)

Solutions

  1. Retry the whole `pushDocUpdates` call after backoff — updates are CRDT-encoded and safe to resend.
  2. Inspect server logs for the original `e` logged under 'Failed to insert doc updates' to find the DB root cause.
  3. Verify DB connectivity, disk space, and connection-pool health.
  4. If caused by deadlocks, reduce concurrent writers per doc (serialize per docId).

Example fix

// before
await adapter.pushDocUpdates(workspaceId, docId, updates, editorId);

// after
async function pushWithRetry(updates, attempt = 0) {
  try {
    return await adapter.pushDocUpdates(workspaceId, docId, updates, editorId);
  } catch (e) {
    if (e instanceof FailedToSaveUpdates && attempt < 3) {
      await sleep(2 ** attempt * 200);
      return pushWithRetry(updates, attempt + 1);
    }
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Coalesce and dedupe updates to reduce DB insert pressure
async function coalesceAndPush(workspaceId: string, docId: string, updates: Uint8Array[]) {
  const deduped = await filterValidDocUpdates(workspaceId, docId, updates);
  if (!deduped.length) return 0;
  return adapter.pushDocUpdates(workspaceId, docId, deduped, editorId);
}

Type guard

function isFailedToSaveUpdates(e: unknown): boolean {
  return (
    typeof e === 'object' && e !== null &&
    (e as { code?: string }).code === 'failed_to_save_updates'
  );
}

Try / catch

async function pushWithRetry(updates: Uint8Array[], attempt = 0) {
  try {
    return await adapter.pushDocUpdates(workspaceId, docId, updates, editorId);
  } catch (e) {
    if (isFailedToSaveUpdates(e) && attempt < 3) {
      await sleep(2 ** attempt * 200);
      return pushWithRetry(updates, attempt + 1);
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: The Postgres `doc.createUpdates` insert repeatedly fails: connection loss, deadlock, constraint violation, disk full, or a transient DB restart that outlasts the `retryable` budget.

Common situations: Database connectivity blips during heavy sync; deadlocks from concurrent writers; storage exhaustion; schema/data drift causing constraint errors; DB maintenance windows.

Related errors


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