duplicati/duplicati · error · Exception

Failed to safe-delete volume {name}, blocks: {c}

Error message

Failed to safe-delete volume {name}, blocks: {c}

What it means

Generic Exception in SafeDeleteRemoteVolumeAsync: it COUNTs Block rows referencing the volume and refuses to delete if any exist (c != 0). The method is intentionally a guard — it only deletes a volume proven to have zero blocks, preventing orphaned block references. Hitting it means the caller tried to delete a volume that still holds block data.

Source

Thrown at Duplicati/Library/Main/Database/Local/LocalBackupDatabase.cs:1847

        /// <param name="token">The cancellation token to cancel the operation.</param>
        /// <returns>A task that completes when the remote volume is safely deleted.</returns>
        /// <exception cref="Exception">Thrown if the volume has associated blocks.</exception>
        public async Task SafeDeleteRemoteVolumeAsync(string name, CancellationToken token)
        {
            var volumeid = await GetRemoteVolumeIDAsync(name, token).ConfigureAwait(false);

            await using var cmd = m_connection.CreateCommand(m_rtr);
            var c = await cmd.SetCommandAndParameters(@"
                    SELECT COUNT(*)
                    FROM ""Block""
                    WHERE ""VolumeID"" = @VolumeId
                ")
                .SetParameterValue("@VolumeId", volumeid)
                .ExecuteScalarInt64Async(-1, token)
                .ConfigureAwait(false);

            if (c != 0)
                throw new Exception($"Failed to safe-delete volume {name}, blocks: {c}");

            await RemoveRemoteVolumeAsync(name, token).ConfigureAwait(false);
        }

        /// <summary>
        /// Retrieves the hashes of blocks that are on the blocklist for a given volume.
        /// </summary>
        /// <param name="name">The name of the volume to check.</param>
        /// <param name="token"> The cancellation token to cancel the operation.</param>
        /// <returns>A task that when awaited contains an array of blocklist hashes.</returns>
        public async Task<string[]> GetBlocklistHashesAsync(string name, CancellationToken token)
        {
            var volumeid = GetRemoteVolumeIDAsync(name, token);
            await using var cmd = m_connection.CreateCommand(m_rtr);
            // Grab the strings and return as array to avoid concurrent access to the IEnumerable
            cmd.SetCommandAndParameters(@"
                    SELECT DISTINCT ""Block"".""Hash""
                    FROM ""Block""

View on GitHub (pinned to 3f348be3e3)

Solutions

  1. Move/remove all blocks off the volume (compaction/reclaim) before safe-deleting it.
  2. Re-run the block-volume cleanup so no blocks reference the target volume.
  3. Serialize operations so no backup adds blocks to the volume mid-delete.
  4. Run database repair to reconcile Block/Volume references.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check the safe-delete precondition:
long n = await CountBlocksOnVolumeAsync(volumeid, token);
if (n != 0)
    throw new InvalidOperationException($"Volume {name} still has {n} blocks; relocate them first.");

Try / catch

try { await db.SafeDeleteRemoteVolumeAsync(name, token); }
catch (Exception ex) when (ex.Message.Contains("Failed to safe-delete volume"))
{ Log.Warn($"Volume {name} still has blocks; run compaction/reclaim before deleting."); /* do not retry unchanged */ }

Prevention

When it happens

Trigger: Call SafeDeleteRemoteVolumeAsync(name) for a volume that still has >=1 Block row referencing it (COUNT(*) WHERE VolumeID = @VolumeId != 0).

Common situations: Calling safe-delete during/after a failed block relocation that left blocks on the volume; race where a backup added blocks to the volume between the empty-check and delete; caller logic deleting the wrong volume; leftover blocks after a partial purge.

Related errors


AI-assisted analysis of duplicati/duplicati@3f348be3e3 (2026-08-13). Data as JSON: /api/errors/ae0e1f50f106343e. Report an issue: GitHub.