Right, let’s talk about S3 Versioning. This is one of those features that sounds simple on the surface—“it keeps multiple versions of an object”—but the devil, as always, is in the details. And the AWS console does its best to hide those details from you, which is why we’re having this chat. Think of versioning as the ultimate “undo” button for your bucket, but an undo button that, by default, just keeps every single change you’ve ever made, forever. This is fantastic for recovery, less fantastic for your storage bill.

First, the cardinal rule: versioning is enabled at the bucket level, not the object level. You flip a switch for the entire bucket, and from that moment on, every new object upload, every overwrite, every deletion is tracked as a distinct version. It’s a one-way door. You can suspend versioning, but you can’t ever truly disable it. Once you’ve enabled it, the genie is out of the bottle. Suspending just means that new objects won’t get a new version ID on creation, but all the old versions are still there, hanging out, waiting to be called upon or, more likely, to quietly drain your AWS budget.

Enabling and Suspending Versioning

You can do this in the AWS console with a few clicks, but that’s for amateurs. We do it in code so it’s repeatable and documented. Here’s how you enable it with the Python SDK (boto3). Notice how you’re just setting the bucket’s Versioning configuration status. It’s painfully straightforward.

import boto3

s3 = boto3.client('s3')
bucket_name = 'my-super-important-bucket'

# Enable versioning
s3.put_bucket_versioning(
    Bucket=bucket_name,
    VersioningConfiguration={'Status': 'Enabled'}
)

# Later, if you regret your life choices (or just want to stop new versions)
s3.put_bucket_versioning(
    Bucket=bucket_name,
    VersioningConfiguration={'Status': 'Suspended'}
)

The Anatomy of a Versioned Delete

This is where most people’s brains short-circuit. When versioning is enabled, you never really delete an object. Let’s say you delete report.pdf. What you’re actually doing is creating a delete marker. This is a special kind of version that sits on top of the stack. When you list the contents of your bucket (a simple list_objects_v2 call), the S3 service sees the delete marker and says, “Yep, the latest version of that file is a deletion, so I won’t show it to you.” It’s a soft delete.

The report.pdf you thought you deleted? All its previous versions are still safely stored. This is brilliant for recovering from accidental deletions. It’s also how you can end up with millions of invisible objects you’re still paying for. The console will show you these deleted objects if you check “Show versions,” which you should always, always do.

Permanently Deleting an Object (The Right Way)

To truly, irrevocably nuke an object from existence and stop paying for it, you must perform a permanent delete. This means you have to delete a specific version of the object, not just its name. You need its VersionId.

First, you need to find that ID. You have to list the object versions to see what you’re working with.

# List all versions of a specific object
response = s3.list_object_versions(
    Bucket=bucket_name,
    Prefix='report.pdf'
)

for version in response.get('Versions', []):
    print(f"VersionId: {version['VersionId']}, IsLatest: {version['IsLatest']}, LastModified: {version['LastModified']}")

for delete_marker in response.get('DeleteMarkers', []):
    print(f"DELETE MARKER - VersionId: {delete_marker['VersionId']}, IsLatest: {delete_marker['IsLatest']}")

This will show you all the live versions and any delete markers. Now, to permanently delete a version—say, an old version with a specific VersionId:

# Permanently delete a specific version
s3.delete_object(
    Bucket=bucket_name,
    Key='report.pdf',
    VersionId='LH3Lg1Q0Wc6ZPrMpFej6Rj.exampleVersionId'
)

And to clean up a delete marker (which makes the object “reappear” because you’ve removed the marker that was hiding the latest live version):

# Delete a delete marker to "undelete" the object
s3.delete_object(
    Bucket=bucket_name,
    Key='report.pdf',
    VersionId='H33Lg1Q0Wc6ZPrMpFej6Rj.exampleDeleteMarkerId'
)

The Gotchas and Best Practices

  1. Cost: This is the big one. You pay for every byte of every version. An object that is overwritten 100 times is stored 100 times. You absolutely must pair versioning with a Lifecycle Policy to move non-current versions to cheaper storage tiers (like Infrequent Access) or to expire them after a sane period. Don’t let this become a data hoarding problem.
  2. The MFA Delete Option: There’s a niche but powerful feature called MFA Delete that forces anyone trying to change the versioning state of the bucket or permanently delete a version to provide a Multi-Factor Authentication code. It’s a pain to set up (it can only be done via the CLI or API, not the console), but for buckets containing truly critical data, it’s a fantastic extra layer of protection against a compromised account.
  3. Listing is Weird: Remember, list_objects (the v1 API) is basically useless here. You must use list_objects_v2 with its caveats, or better yet, list_object_versions to see what’s actually in your bucket.

Versioning is arguably S3’s best feature for data durability, but it’s not a set-it-and-forget-it thing. It’s a powerful tool that requires active management. Enable it, use it, but for the love of your CFO, manage it with lifecycle rules.