kopia/kopia · error

unable to schedule delayed cleanup of blobs

Error message

unable to schedule delayed cleanup of blobs

What it means

ManagerV0.deleteOldBlobs fails to schedule deferred deletion of compaction-log blobs. After deleting old index blobs, it must write a cleanup-entry blob recording which compaction logs to delete later; if delayCleanupBlobs returns an error, this wrapper is returned to registerCompaction and thus to Compact. Compaction itself succeeded, but garbage-collection bookkeeping failed, so old compaction logs linger and cleanup is not scheduled.

Solutions

  1. Inspect the wrapped inner error for the actual storage failure (credentials, connectivity, quota) and fix it.
  2. Retry Compact(); delayed-cleanup scheduling is idempotent-safe to retry since it only writes a cleanup entry blob.
  3. Verify the storage destination is writable and not in a read-only or expired state.
  4. Check server clock sanity: cleanup schedule time comes from the latest blob's server timestamp.

Example fix

// before
if err := m.delayCleanupBlobs(ctx, compactionLogBlobsToDelayCleanup, latestBlob.Timestamp); err != nil {
	return errors.Wrap(err, "unable to schedule delayed cleanup of blobs")
}
// after
if err := m.delayCleanupBlobs(ctx, compactionLogBlobsToDelayCleanup, latestBlob.Timestamp); err != nil {
	m.log.Errorw("delayed cleanup scheduling failed; will retry on next compaction", "err", err)
	return errors.Wrap(err, "unable to schedule delayed cleanup of blobs")
}
Defensive patterns

Strategy: retry

Validate before calling

// Go: check storage writability before Compact
if err := storageTest(ctx, blobStore); err != nil {
	return fmt.Errorf("blob storage not writable: %w", err)
}

Try / catch

err := mgr.Compact(ctx); if err != nil && strings.Contains(err.Error(), "unable to schedule delayed cleanup") { // retry after storage recovers
	retry.Do(func() error { return mgr.Compact(ctx) }, retry.Attempts(3))
}

Prevention

When it happens

Trigger: Calling Compact() on an index blob manager when the underlying storage write in delayCleanupBlobs (EncryptAndWriteBlob with V0CleanupBlobPrefix) fails, or JSON marshaling of the cleanupEntry fails; the inner error is wrapped with this message by deleteOldBlobs.

Common situations: Blob storage backend outage or throttling during compaction (S3/Blob Storage 5xx), expired or revoked cloud credentials, network partition between the kopia client and the blob store, or a read-only/misconfigured storage destination.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of kopia/kopia@82495e54b5 (2026-09-07). Data as JSON: /api/errors/3ac944ec8dd4689f. Report an issue: GitHub.

Appendix: source

Thrown at repo/content/indexblob/index_blob_manager_v0.go:324

		logparam.Time("compactionLogServerTimeCutoff", compactionLogServerTimeCutoff))

	compactionBlobEntries, err := m.getCompactionLogEntries(ctx, compactionBlobs)
	if err != nil {
		return errors.Wrap(err, "unable to get compaction log entries")
	}

	indexBlobsToDelete := m.findIndexBlobsToDelete(ctx, latestBlob.Timestamp, compactionBlobEntries, maxEventualConsistencySettleTime)

	// note that we must always delete index blobs first before compaction logs
	// otherwise we may inadvertently resurrect an index blob that should have been removed.
	if err := m.deleteBlobsFromStorageAndCache(ctx, indexBlobsToDelete); err != nil {
		return errors.Wrap(err, "unable to delete compaction logs")
	}

	compactionLogBlobsToDelayCleanup := m.findCompactionLogBlobsToDelayCleanup(ctx, compactionBlobs)

	if err := m.delayCleanupBlobs(ctx, compactionLogBlobsToDelayCleanup, latestBlob.Timestamp); err != nil {
		return errors.Wrap(err, "unable to schedule delayed cleanup of blobs")
	}

	return nil
}

func (m *ManagerV0) findIndexBlobsToDelete(ctx context.Context, latestServerBlobTime time.Time, entries map[blob.ID]*compactionLogEntry, maxEventualConsistencySettleTime time.Duration) []blob.ID {
	tmp := map[blob.ID]bool{}

	for _, cl := range entries {
		// are the input index blobs in this compaction eligible for deletion?
		if age := latestServerBlobTime.Sub(cl.metadata.Timestamp); age < maxEventualConsistencySettleTime {
			contentlog.Log3(ctx, m.log,
				"not deleting compacted index blob used as inputs for compaction",
				blobparam.BlobID("blobID", cl.metadata.BlobID),
				logparam.Duration("age", age),
				logparam.Duration("maxEventualConsistencySettleTime", maxEventualConsistencySettleTime))

			continue

View on GitHub (pinned to 82495e54b5)