AWS S3 Versioning
How S3 versioning protects against overwrites and deletes. Delete markers, the storage cost nobody expects, lifecycle rules and Object Lock.
Versioning keeps every version of every object in a bucket. An overwrite creates a new version rather than replacing the old one, and a delete hides the object rather than removing it.
It is the difference between "someone overwrote the production config" being a five-minute recovery and being an incident.
aws s3api put-bucket-versioning \
--bucket acme-artifacts \
--versioning-configuration Status=Enabled
It cannot be turned off
Once enabled, versioning can be suspended but never returned to the unversioned state. Suspending stops new versions being created; it does not delete the versions already stored, which remain and continue to be billed.
That is worth knowing before enabling it on a bucket holding terabytes of frequently-overwritten data.
| State | New writes | Existing versions |
|---|---|---|
Enabled | Create new versions | Retained |
Suspended | Overwrite the null version | Retained and billed |
| Never enabled | Overwrite in place | N/A |
Deletes create a marker, not a deletion
This is the mechanism to understand, because it explains both the protection and the cost.
A DELETE on a versioned bucket does not remove anything. It adds a delete marker, a zero-byte object that becomes the current version, so the object appears gone from a normal listing while every previous version is still stored.
# Looks deleted
aws s3 ls s3://acme-artifacts/config.json
# Actually still there
aws s3api list-object-versions --bucket acme-artifacts --prefix config.json
{
"Versions": [
{ "VersionId": "3HL4kqtJlcpXroDTDmpUMLUo", "IsLatest": false, "Size": 2048 }
],
"DeleteMarkers": [
{ "VersionId": "Z9sUq2rD8yTyW1fEz3aBcD", "IsLatest": true }
]
}
To restore, delete the delete marker:
aws s3api delete-object \
--bucket acme-artifacts \
--key config.json \
--version-id Z9sUq2rD8yTyW1fEz3aBcD
The object reappears immediately. That asymmetry, deleting a delete marker undeletes the object, is initially confusing and is the whole recovery mechanism.
Deleting a specific --version-id of the actual data is permanent.
The cost surprise
Every version is billed at full storage price. A 100 MB file overwritten daily for a year is 36 GB of storage, not 100 MB.
This catches almost everyone: versioning is enabled for safety, nightly jobs overwrite objects, and storage grows steadily for months while a normal aws s3 ls shows a bucket that looks small.
# What is actually stored, versions included
aws s3api list-object-versions --bucket acme-artifacts \
--query 'sum(Versions[].Size)' --output text
The fix is a lifecycle rule, and every versioned bucket should have one:
{
"Rules": [
{
"ID": "expire-old-versions",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"NoncurrentVersionTransitions": [
{ "NoncurrentDays": 30, "StorageClass": "STANDARD_IA" }
],
"NoncurrentVersionExpiration": { "NoncurrentDays": 90 },
"Expiration": { "ExpiredObjectDeleteMarker": true },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
Three parts, all worth having:
NoncurrentVersionExpirationremoves old versions after a retention window. Without it they accumulate forever.ExpiredObjectDeleteMarkercleans up delete markers whose underlying versions have all expired, which otherwise linger as clutter and slow listings.AbortIncompleteMultipartUploadremoves orphaned upload parts, which are billed as storage and never appear in a listing.
aws s3api put-bucket-lifecycle-configuration \
--bucket acme-artifacts --lifecycle-configuration file://lifecycle.json
Listing gets slower
list-object-versions returns every version and every delete marker, so a bucket with millions of accumulated versions is slow and expensive to enumerate. Lifecycle expiry keeps this manageable; without it, listing performance degrades quietly over years.
MFA delete
For high-value buckets, MFA delete requires a hardware or virtual MFA token to permanently delete a version or to change the versioning state:
aws s3api put-bucket-versioning \
--bucket acme-critical \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::111122223333:mfa/root-account-mfa-device 123456"
Two constraints make it awkward: it can only be configured by the root user of the account, and it cannot be enabled through the console. That limits it to genuinely critical buckets, but for those it is a strong protection. An attacker with compromised credentials still cannot permanently destroy data.
Object Lock for true immutability
Versioning protects against accident. Object Lock protects against intent.
With Object Lock in compliance mode, an object cannot be deleted or overwritten by anyone, including the root user, until its retention period expires. This is what ransomware protection and regulatory retention actually require.
It must be enabled when the bucket is created and requires versioning. It cannot be added to an existing bucket, which is a decision to make up front for anything holding backups.
Governance mode is the softer variant, allowing users with a specific permission to override retention, which suits internal policy rather than compliance.
Versioning and replication
Cross-region replication requires versioning on both source and destination buckets. It is worth noting that replication copies new objects going forward, existing objects need a separate batch replication job, and that delete markers are only replicated if you configure it explicitly.
Quick reference
| Task | Command |
|---|---|
| Enable | aws s3api put-bucket-versioning --bucket <b> --versioning-configuration Status=Enabled |
| Check status | aws s3api get-bucket-versioning --bucket <b> |
| List all versions | aws s3api list-object-versions --bucket <b> --prefix <key> |
| Download a version | aws s3api get-object --bucket <b> --key <k> --version-id <v> out.txt |
| Undelete | aws s3api delete-object --bucket <b> --key <k> --version-id <delete-marker-id> |
| Permanently delete a version | aws s3api delete-object --bucket <b> --key <k> --version-id <version-id> |
| Total versioned size | aws s3api list-object-versions --bucket <b> --query 'sum(Versions[].Size)' |
| Apply lifecycle | aws s3api put-bucket-lifecycle-configuration --bucket <b> --lifecycle-configuration file://lifecycle.json |
Key takeaways
- Versioning cannot be disabled, only suspended, and suspending does not delete existing versions.
- A delete creates a delete marker. The data is still stored and still billed.
- Undelete by deleting the delete marker, which restores the previous version instantly.
- Every version is billed at full price. A daily-overwritten file grows storage indefinitely.
- Every versioned bucket needs a lifecycle rule with
NoncurrentVersionExpirationandExpiredObjectDeleteMarker. - MFA delete needs the root user and cannot be set in the console.
- Object Lock must be enabled at bucket creation and is what provides genuine immutability.
Check your understanding
Restoring a deleted object
An object was deleted from a versioned bucket. How do you restore it?
Storage that keeps growing
A versioned bucket holds a 100 MB file overwritten by a nightly job. After a year, aws s3 ls shows 100 MB but the bill reflects far more. Why?
Frequently Asked Questions
How do I restore a deleted object in S3?
Find the delete marker with aws s3api list-object-versions --bucket <name> --prefix <key>, then delete that marker by its version ID. Removing the marker makes the previous version current and the object reappears immediately. If the specific data version itself was deleted with a version ID rather than a plain delete, it is permanently gone, only a delete marker is reversible.
Why is my versioned S3 bucket so expensive?
Because every version is stored and billed at full price, and a normal listing shows only current versions so the growth is invisible. A file overwritten nightly accumulates a year of copies. Check the real total with aws s3api list-object-versions --bucket <name> --query 'sum(Versions[].Size)'. The fix is a lifecycle rule with NoncurrentVersionExpiration to remove old versions after a retention window, plus ExpiredObjectDeleteMarker to clean up orphaned markers.
Can I turn off S3 versioning?
Not completely. Versioning can be suspended, which stops new versions being created, but a bucket can never return to the unversioned state. Suspending also does not delete existing versions. They remain stored and billed until a lifecycle rule expires them. Enable versioning deliberately, particularly on buckets holding large, frequently-overwritten objects, and pair it with a lifecycle policy from the start.
What is a delete marker?
A zero-byte object that S3 adds as the new current version when you delete an object in a versioned bucket. It makes the object appear absent from normal listings while every previous version remains stored. Delete markers themselves consume negligible space but accumulate as clutter, slow down version listings, and are worth cleaning up with the ExpiredObjectDeleteMarker lifecycle action once their underlying versions have expired.
What is the difference between versioning and Object Lock?
Versioning protects against accidents. An overwrite or delete is recoverable, but anyone with permission can still permanently remove a specific version. Object Lock protects against intent: in compliance mode an object cannot be deleted or modified by anyone, including the root user, until its retention period expires. Object Lock requires versioning and must be enabled when the bucket is created, so it is a decision to make up front for backups and anything with regulatory retention requirements.
Should I enable MFA delete?
For genuinely critical buckets, yes. It requires an MFA token to permanently delete a version or change the versioning configuration, so compromised credentials alone cannot destroy data. Two constraints limit its use: it can only be configured by the account's root user, and not through the console. That makes it impractical as a blanket policy but worthwhile for a small number of high-value buckets. For broader immutability, Object Lock is usually the more manageable option.