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

  1. Read the preceding server log line 'Failed to insert doc updates' — it contains the underlying database error that this wrapper hides
  2. Check database health, connectivity, and that all migrations are applied (prisma migrate deploy)
  3. Retry the push after the DB issue is fixed — client-side Yjs updates are still buffered locally and can be re-pushed
  4. 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

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


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)