apache/iceberg · warning

Delete failed for {} file: {}

Error message

Delete failed for {} file: {}

What it means

In the per-file (non-bulk) delete path, Tasks retries each deletion 3 times (stopping on NotFoundException); if a file still fails after retries, this WARN 'Delete failed for {fileType} file: {file}' is logged per file. The operation continues with the remaining files; the failed file may become an orphan that a later cleanup run must remove.

Source

Thrown at core/src/main/java/org/apache/iceberg/FileCleanupStrategy.java:131

            "Bulk deletion failed for {} of {} {} file(s)",
            e.numberFailedObjects(),
            pathsToDelete.size(),
            fileType,
            e);
      } catch (RuntimeException e) {
        LOG.warn("Bulk deletion failed", e);
      }
    } else {
      Consumer<String> deleteFuncToUse = deleteFunc == null ? defaultDeleteFunc : deleteFunc;

      Tasks.foreach(pathsToDelete)
          .executeWith(deleteExecutorService)
          .retry(3)
          .stopRetryOn(NotFoundException.class)
          .stopOnFailure()
          .suppressFailureWhenFinished()
          .onFailure(
              (file, thrown) -> LOG.warn("Delete failed for {} file: {}", fileType, file, thrown))
          .run(deleteFuncToUse::accept);
    }
  }

  protected boolean hasAnyStatisticsFiles(TableMetadata tableMetadata) {
    return !tableMetadata.statisticsFiles().isEmpty()
        || !tableMetadata.partitionStatisticsFiles().isEmpty();
  }

  protected Set<String> expiredStatisticsFilesLocations(
      TableMetadata beforeExpiration, TableMetadata afterExpiration) {
    Set<String> statsFileLocationsBeforeExpiration = statsFileLocations(beforeExpiration);
    Set<String> statsFileLocationsAfterExpiration = statsFileLocations(afterExpiration);

    return Sets.difference(statsFileLocationsBeforeExpiration, statsFileLocationsAfterExpiration);
  }

  private Set<String> statsFileLocations(TableMetadata tableMetadata) {

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Inspect the logged exception for the specific file to determine the storage-side cause (403 vs 5xx vs timeout).
  2. Re-run snapshot expiration later — file cleanup is idempotent and already-deleted files stop retrying via NotFoundException.
  3. Verify IAM/storage permissions for the exact prefix of failing files.
  4. Lower concurrency (deleteExecutorService) if deletions are being throttled.
Defensive patterns

Strategy: retry

Validate before calling

// verify FileIO can delete in the target prefix before large cleanups
String probe = prefix + "/__probe_" + UUID.randomUUID();
io.deleteFile(probe);

Try / catch

try {
  table.expireSnapshots().execute();
} catch (RuntimeException e) {
  LOG.warn("Snapshot expiration finished with per-file delete warnings", e);
}

Prevention

When it happens

Trigger: expireSnapshots/deleteFiles runs with a custom deleteFunc or default delete path, and deleting a single path throws (transient S3/GCS error, permission denied, throttling) on all 3 retries; NotFoundException short-circuits as the file is already gone.

Common situations: Object-store eventual consistency where the file was already removed by a concurrent expiration; IAM policies denying DeleteObject on some prefixes; network blips during large snapshot expiration.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/6bd8abe25001475c. Report an issue: GitHub.