Back to S3 & Storage
Q4Question 4 of 5

S3 Versioning, Delete Markers, Lifecycle, and MFA Delete

Key Concepts
  • S3 versioning keeps every version of an object so accidental overwrites and deletes are fully recoverable.
  • Deleting an object in a versioned bucket inserts a delete marker instead of erasing data, and removing the marker restores the object.
  • Permanently erasing data requires deleting a specific version ID, which is a deliberate separate operation.
  • Lifecycle rules target noncurrent versions to transition or expire them, controlling cost without losing the current version.
  • MFA Delete requires a multi-factor authentication code to permanently delete a version or disable versioning, guarding against compromised credentials.
  • Versioning cannot be turned off once enabled, only suspended, and suspended keeps existing versions but stops creating new ones.
  • For true WORM compliance, Object Lock in compliance mode is preferred over MFA Delete alone.

Definition

Versioning keeps every version and uses a delete marker instead of erasing, so accidental overwrites and deletes are recoverable, while lifecycle rules clean up old versions and MFA Delete guards permanent deletion.
  • S3 versioning – a bucket-level setting that keeps every version of an object instead of overwriting it; each PUT creates a new version with a unique version ID while older versions are retained.
  • Delete marker – when you delete an object in a versioned bucket, S3 inserts a delete marker that hides the object rather than erasing data; removing the marker restores the object.
  • Noncurrent versions – previous versions that are no longer the current one; lifecycle rules can transition or expire them independently.
  • MFA Delete – a setting that requires a multi-factor authentication code to permanently delete a version or disable versioning.
  • Object Lock – a complementary feature that provides write-once-read-many (WORM) compliance in governance or compliance mode.

Imagine National Bank of India keeps a customer's KYC document. Without versioning, if a clerk uploads a wrong file over the right one, the original is gone forever. With versioning, the warehouse keeps every copy ever filed, stamped with a version number, so the old correct copy is still there. If someone "deletes" the document, the warehouse does not shred it -- it just puts a "hidden" sticker (a delete marker) on top, and a supervisor can peel off the sticker to get the document back. Lifecycle rules are the warehouse policy that shreds very old hidden copies to save space, and MFA Delete is the rule that permanently shredding anything requires two keys turned at once by an authorized manager.

Understanding the Concepts

What is S3 versioning?

S3 versioning is a bucket-level setting that preserves every version of every object. When enabled, each upload to the same key creates a new current version with a unique version ID, and the previous version becomes noncurrent but is still stored and accessible by its ID.

JAVA
package com.bank.storage;

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import java.nio.file.Path;

public class VersionedUploader {
    private final S3Client s3 = S3Client.create();

    public String uploadKycDocument(String customerId, Path filePath) {
        PutObjectResponse resp = s3.putObject(
            PutObjectRequest.builder()
                .bucket("nbi-kyc")
                .key(customerId + "/pan-card.pdf")
                .build(),
            filePath);
        String versionId = resp.versionId(); // e.g. "3HL4kqtJ..."
        System.out.println("Uploaded version: " + versionId);
        return versionId;
    }
}

How do delete markers work?

Deletion behaves specially on a versioned bucket. A simple delete does not remove any data. Instead, S3 writes a delete marker as the new current version, so a normal GET returns a 404 as if the object is gone. The actual data versions still exist underneath. To restore, you delete the delete marker by its version ID, and the previous version becomes current again.

JAVA
package com.bank.storage;

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;

public class DeleteMarkerRecovery {
    private final S3Client s3 = S3Client.create();

    public void restoreDeletedObject(String bucket, String key, String deleteMarkerVersionId) {
        // Remove the delete marker to un-hide the object
        s3.deleteObject(DeleteObjectRequest.builder()
            .bucket(bucket)
            .key(key)
            .versionId(deleteMarkerVersionId)
            .build());
        System.out.println("Delete marker removed. Object restored: " + key);
    }

    public void permanentlyDelete(String bucket, String key, String specificVersionId) {
        // This truly erases the version -- deliberate, separate action
        s3.deleteObject(DeleteObjectRequest.builder()
            .bucket(bucket)
            .key(key)
            .versionId(specificVersionId)
            .build());
        System.out.println("Version permanently deleted: " + specificVersionId);
    }
}

How does versioning protect against accidental overwrites?

When someone uploads a wrong file over the correct one, the correct version is retained as a noncurrent version. You can list all versions of an object, identify the correct one by its version ID and timestamp, and either download it directly or make it current again by copying it back with a new PUT.

JAVA
package com.bank.storage;

import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import java.util.List;

public class VersionLister {
    private final S3Client s3 = S3Client.create();

    public void listVersions(String bucket, String key) {
        ListObjectVersionsResponse resp = s3.listObjectVersions(
            ListObjectVersionsRequest.builder()
                .bucket(bucket).prefix(key).build());
        List<ObjectVersion> versions = resp.versions();
        for (ObjectVersion v : versions) {
            System.out.println("Version: " + v.versionId()
                + " | Modified: " + v.lastModified()
                + " | Current: " + v.isLatest());
        }
    }
}

How do lifecycle rules work with versioning?

Keeping every version forever is expensive. Lifecycle rules can target noncurrent versions specifically: transition noncurrent versions to a cheaper class after a number of days, and permanently expire noncurrent versions after a safe period, while the current version stays untouched. Lifecycle rules can also clean up expired delete markers and incomplete multipart uploads.

JAVA
// Lifecycle rule targeting noncurrent versions
package com.bank.storage;

import software.amazon.awssdk.services.s3.model.*;
import java.util.List;

public class NoncurrentVersionLifecycle {
    public static LifecycleRule buildNoncurrentRule() {
        return LifecycleRule.builder()
            .id("expire-old-kyc-versions")
            .filter(LifecycleRuleFilter.builder().prefix("kyc/").build())
            .status(ExpirationStatus.ENABLED)
            .noncurrentVersionTransitions(List.of(
                NoncurrentVersionTransition.builder()
                    .noncurrentDays(30)
                    .storageClass(TransitionStorageClass.STANDARD_IA)
                    .build()))
            .noncurrentVersionExpiration(
                NoncurrentVersionExpiration.builder()
                    .noncurrentDays(90)
                    .build())
            .build();
    }
}

What is MFA Delete?

MFA Delete adds a strong safeguard. When enabled, two sensitive operations require a multi-factor authentication code from a hardware or virtual device: permanently deleting a specific object version, and disabling or suspending versioning on the bucket. This stops even a compromised set of credentials from wiping version history.

BASH
# MFA Delete can only be set by the root account via CLI
aws s3api put-bucket-versioning \
  --bucket nbi-kyc \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::123456789012:mfa/root-device 123456"
JAVA
// MFA Delete prevents programmatic mass deletion
package com.bank.storage;

public class MfaDeleteGuard {
    public static void explain() {
        System.out.println("With MFA Delete enabled:");
        System.out.println("- Deleting a specific version ID requires an MFA code");
        System.out.println("- Disabling versioning requires an MFA code");
        System.out.println("- Even compromised admin credentials cannot wipe history");
    }
}

What is Object Lock and how does it compare to MFA Delete?

Object Lock provides write-once-read-many (WORM) compliance. In compliance mode, no one, not even the root account, can delete a locked object before its retention period expires. In governance mode, users with special permissions can override the lock. Object Lock is preferred for regulatory compliance because it is stricter than MFA Delete, which only adds a second factor rather than a hard block.

JAVA
package com.bank.storage;

public class RetentionComparison {
    public static void compare() {
        System.out.println("MFA Delete: requires MFA code to delete a version");
        System.out.println("  - Root account can still delete with the code");
        System.out.println("  - Can be disabled by root with MFA");
        System.out.println();
        System.out.println("Object Lock (Compliance mode): nobody can delete");
        System.out.println("  - Not even root can override until retention expires");
        System.out.println("  - Required for regulatory WORM compliance");
    }
}

What are the rules and constraints?

Versioning cannot be turned off once enabled, only suspended. Suspending keeps existing versions intact but stops creating new ones for future uploads. Versioning must be enabled before Object Lock can be used. Delete markers are zero-size objects that consume no storage but do count as objects. Each noncurrent version occupies its full size in storage, so versioning can significantly increase storage costs without lifecycle management.

JAVA
package com.bank.storage;

public class VersioningRules {
    public static void printRules() {
        System.out.println("1. Versioning cannot be disabled, only suspended");
        System.out.println("2. Suspended: existing versions kept, no new ones created");
        System.out.println("3. Delete markers are zero-size but count as objects");
        System.out.println("4. Each noncurrent version occupies full storage");
        System.out.println("5. Object Lock requires versioning to be enabled first");
        System.out.println("6. MFA Delete only manageable by root via CLI/API");
    }
}

What are common mistakes?

The most common mistake is enabling versioning without a noncurrent-version lifecycle rule, so every overwrite accumulates hidden versions that silently inflate the storage bill. A second mistake is assuming a simple delete actually removes data. A third is relying on MFA Delete alone for compliance when Object Lock is the correct tool.

JAVA
// Wrong: versioning enabled, no lifecycle rule
// 1000 objects overwritten daily for a year = 365,000 noncurrent versions
// All in Standard, paying full price for data nobody needs

// Correct: versioning + noncurrent version lifecycle rule
// Noncurrent versions transition to Standard-IA after 30 days
// Noncurrent versions expire after 90 days
// Cost controlled, recoverability preserved for 90 days

Visual Overview

DIAGRAM
              S3 Versioning: Upload, Delete, and Restore Flow

  Upload v1          Upload v2           Delete            Restore
  ─────────          ─────────          ──────            ───────
  ┌─────────┐       ┌─────────┐       ┌─────────┐       ┌─────────┐
  │ v1      │       │ v2 (cur)│       │ DM (cur)│       │ v2 (cur)│
  │ (cur)   │       │ v1      │       │ v2      │       │ v1      │
  └─────────┘       └─────────┘       │ v1      │       └─────────┘
                                      └─────────┘
  GET → v1 data     GET → v2 data     GET → 404        GET → v2 data
                    v1 still there!   Data still there! DM removed by
                                      Hidden by marker  version ID


          Lifecycle + Versioning Cost Control

  ┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
  │  Current   │    │ Noncurrent │    │ Noncurrent │    │  EXPIRED   │
  │  version   │    │  version   │    │  in S3-IA  │    │ (deleted)  │
  │ (Standard) │    │ (Standard) │    │            │    │            │
  └────────────┘    └─────┬──────┘    └─────┬──────┘    └────────────┘
       │                  │ 30 days         │ 90 days
       │                  ▼                 ▼
       │           ┌────────────┐    ┌────────────┐
  stays current    │ Transition │    │   Expire   │
                   │  to S3-IA  │    │  noncurrent│
                   └────────────┘    └────────────┘

Key Takeaway

S3 versioning is a bucket-level setting that keeps every version of an object rather than overwriting it. Each upload to the same key creates a new current version with a unique version ID, and previous versions are retained as noncurrent versions that you can still read by their IDs. This is the foundation of protecting data against accidental changes.

The clever part is how deletion works. On a versioned bucket a normal delete does not erase anything; it writes a delete marker that becomes the current version, so a plain read returns a 404 as if the object is gone, but the real data sits underneath. To recover, you delete the delete marker by its version ID and the previous version becomes current again. To genuinely erase data you must delete a specific version ID, which is a deliberate, separate action.

Versioning combines with lifecycle rules and MFA Delete to stay safe and affordable. Keeping every version forever is expensive, so I write lifecycle rules that target noncurrent versions specifically, transitioning them to cheaper classes and expiring them after a safe period, and cleaning up expired delete markers and incomplete uploads. MFA Delete adds a hard safeguard: permanently deleting a version or disabling versioning requires a multi-factor code from the root account's device, so even stolen credentials cannot wipe the audit history. For true WORM compliance, Object Lock in compliance mode is even stronger because nobody, not even root, can delete a locked object before its retention expires.

At National Bank of India we stored KYC documents in a versioned S3 bucket. When a clerk overwrote Rajesh's correct PAN card with the wrong file, the original was still there as a noncurrent version and I restored it in seconds. I set a lifecycle rule to expire noncurrent versions after ninety days to control cost, and protected the bucket with Object Lock in compliance mode for regulatory retention.

Remember one sentence: S3 versioning keeps every version and uses delete markers instead of erasing, so accidental overwrites and deletes are fully recoverable, and lifecycle rules on noncurrent versions keep the cost under control.

---

Share
Use ← → arrow keys to navigate