AWS S3 Versioning Explained: Recovery, Costs & MFA Delete
S3 Versioning preserves every copy of every object in a bucket, so an accidental overwrite or deletion can be reversed without restoring from backup. Every upload to a versioned bucket creates a new version alongside the previous one. Deletions create a delete marker rather than destroying data.
By the end of this page you will understand the three bucket versioning states, what version IDs and null versions are, exactly how delete markers work and how to undo them, how MFA Delete protects against credential compromise, and how to keep versioning costs manageable with lifecycle rules.
Simple explanation
By default, uploading a file to Amazon S3 with the same key as an existing object permanently replaces it. The previous content is gone with no undo. There is no recycle bin and no way to retrieve it.
When versioning is enabled on a bucket, that behaviour changes. Each new upload of the same key creates a separate copy alongside the existing one. The old copy does not disappear. It becomes a noncurrent version with its own unique version ID. Deleting a versioned object works the same way: instead of removing the content, S3 inserts a delete marker. The real data survives underneath, retrievable at any time.
Think of versioning like revision history in a collaborative document. When you save a new draft, the previous one is not deleted. It gets filed in the history with a revision number. The current document is what everyone reads by default. Deleting the document moves it to the trash, but the full history stays intact until you empty it. S3 Versioning works the same way: the latest upload is what S3 returns by default, and every previous copy is in the history, accessible by its version ID.
How S3 Versioning works
Bucket states: unversioned, enabled, suspended
Every S3 bucket is in exactly one of three versioning states:
Unversioned (default). No versions are tracked. Uploading a file with the same key permanently replaces the previous one. This is the default for every new bucket.
Versioning-enabled. Every object upload creates a new version with a unique version ID. Overwrites and deletes are non-destructive until you explicitly remove a version by its ID. Once a bucket transitions to enabled, it cannot return to unversioned. It can only move to suspended.
Versioning-suspended. New uploads no longer create additional versioned copies. They write to a null version ID, overwriting any existing null version for that key. Existing versions with real version IDs are untouched and still billed. Suspension is used to pause new version accumulation without losing existing history.
The direction of travel is one-way: unversioned → enabled → suspended. You can suspend an enabled bucket, and re-enable a suspended one, but you can never return a bucket to a fully unversioned state. Plan before you enable, because you cannot undo it.
Current version, noncurrent version, version IDs, and null versions
When versioning is enabled, every object upload receives a unique alphanumeric version ID. This is how S3 distinguishes one copy of an object from another. The current version is the most recent upload, which is what S3 returns on a normal GET. Every earlier copy is a noncurrent version. Both current and noncurrent versions occupy storage and incur charges.
A null version is any object with a null version ID. Two situations produce null versions:
Objects that existed before you enabled versioning. They were created before version IDs were assigned, so their version ID is null. They behave as the current version for that key.
Uploads after versioning is suspended. New uploads in a suspended bucket write to the null version ID, replacing any existing null version for that key without creating a new version ID.
Null versions can be addressed and deleted by specifying —version-id null in
CLI commands. They are not exempt from storage charges.
What happens on overwrite
When you upload a file to a versioned bucket and a current version already exists, S3 assigns a new version ID to the incoming object and makes it current. The previous current version becomes noncurrent and keeps its original version ID. Both versions are stored and billed.
What happens on delete
Deleting an object in a versioned bucket without specifying a version ID does not remove any content. S3 inserts a delete marker: a lightweight placeholder with its own version ID and no data body. Normal S3 operations (GET, LIST) behave as if the object does not exist, because the delete marker is now the current version. All underlying data survives as noncurrent versions.
Running aws s3 rm s3://my-bucket/config.json on a versioned bucket does
not permanently delete the object. It creates a delete marker. The object is
hidden from normal listings but all previous versions remain in storage and continue to incur
charges. To permanently delete a specific version, you must supply its version ID.
What changes when you overwrite or delete an object
Here is the exact before/after for a file called config.json, showing how
behaviour differs with versioning off and on.
Versioning off (default)
- You upload
config.json. It is stored with no version ID. - You upload a new
config.json. The previous content is permanently overwritten. No recovery possible. - You delete
config.json. The object is permanently removed. No recovery possible.
Versioning on
- You upload
config.json. It is stored with version IDv1abc…(current). - You upload a new
config.json. It receives version IDv2def…(current). Versionv1abc…becomes noncurrent. Both are stored and billed. - You delete
config.jsonwithout specifying a version ID. S3 creates a delete marker with version IDv3ghi…. Normal GETs return 404. Versionsv1abc…andv2def…are still stored, still billed, and fully recoverable. - To recover: delete the delete marker (
v3ghi…) and versionv2def…returns as the current object automatically. - To permanently delete: remove each version by its version ID using
delete-object —version-id.
How to enable, check, and suspend versioning
Versioning is configured at the bucket level and applies to all objects in that bucket. You cannot enable it for individual objects only. Enabling on an existing bucket does not retroactively version existing objects. They become null versions going forward.
# Enable versioning on a bucket
aws s3api put-bucket-versioning \
--bucket my-company-assets \
--versioning-configuration Status=Enabled
# Check the current versioning state
aws s3api get-bucket-versioning --bucket my-company-assets
# Suspend versioning (existing versions are kept; new uploads write to null version ID)
aws s3api put-bucket-versioning \
--bucket my-company-assets \
--versioning-configuration Status=SuspendedSuspending versioning does not remove existing versions. They remain in storage and continue to incur charges. New uploads while suspended overwrite the null version for that key without creating additional versioned copies. Versions with real version IDs created before suspension remain intact.
Configure lifecycle rules for noncurrent versions at the same time as you enable versioning. Waiting until later means weeks of unchecked version accumulation, especially on active buckets with frequent uploads. For guidance on using the AWS CLI to copy and manage objects in a versioned bucket, see the uploading files with the AWS CLI guide.
How to view, restore, and permanently delete versions
Most S3 CLI commands operate on the current version by default. To interact with a specific
version, or to find and remove delete markers, retrieve version IDs from the
list-object-versions output first.
# List all versions and delete markers for all objects in a bucket
aws s3api list-object-versions \
--bucket my-company-assets
# List versions for a specific object key
aws s3api list-object-versions \
--bucket my-company-assets \
--prefix config.jsonThe output contains two arrays: Versions (the actual object copies) and
DeleteMarkers (the placeholders). A delete marker has IsLatest: true
and no content body. If a delete marker appears as the latest entry, the object is logically
deleted but the data survives underneath.
Logical delete vs permanent delete. A logical delete creates a delete marker and hides the object; nothing is removed. A permanent delete specifies a version ID and removes that exact copy irreversibly.
# Restore a deleted object by removing its delete marker
# Get the delete marker version ID from list-object-versions first
aws s3api delete-object \
--bucket my-company-assets \
--key config.json \
--version-id "DELETE_MARKER_VERSION_ID"
# Restore a previous version by copying it back as the current version
aws s3api copy-object \
--bucket my-company-assets \
--copy-source "my-company-assets/config.json?versionId=PREVIOUS_VERSION_ID" \
--key config.json
# Permanently delete a specific version (irreversible)
aws s3api delete-object \
--bucket my-company-assets \
--key config.json \
--version-id "SPECIFIC_VERSION_ID"Object was deleted? Run list-object-versions, find the DeleteMarkers
entry with IsLatest: true, copy its VersionId, then run
delete-object —version-id <that ID>. The previous version resurfaces
automatically. No data was ever gone.
Noncurrent versions continue to incur storage charges until you explicitly delete them or a
lifecycle rule removes them. Run list-object-versions periodically to audit what
is accumulating in active buckets. The S3
cost optimisation guide covers how to identify and reduce version-related storage costs.
MFA Delete
MFA Delete is an optional additional protection layer for a versioned bucket. When enabled, two specific operations require a valid MFA code in addition to valid AWS credentials:
- Permanently deleting a specific object version by supplying a version ID
- Changing the bucket’s versioning state (for example, from Enabled to Suspended)
Normal operations (GET, PUT, LIST, creating delete markers) do not require MFA. The goal is to prevent a compromised access key from permanently wiping all versioned data or silently disabling versioning.
Critical restriction: MFA Delete can only be enabled or disabled by the bucket owner using the root account. IAM users and roles cannot enable or disable MFA Delete, even with full S3 administrator permissions. The CLI command must be run with root account credentials and must include the MFA device serial number and the current one-time code.
# Enable MFA Delete (root account only, active MFA session required)
aws s3api put-bucket-versioning \
--bucket my-company-assets \
--versioning-configuration "Status=Enabled,MFADelete=Enabled" \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"Enabling MFA Delete is straightforward. Disabling it requires root account credentials plus a valid MFA code from the same device. If you lose access to the root MFA device with MFA Delete enabled, permanently deleting versions or changing versioning state will be blocked until you recover through AWS Support. Confirm your root account MFA recovery process before enabling MFA Delete on any production bucket. Use AWS CloudTrail to audit which principals are attempting permanent version deletes or versioning-state changes on your buckets.
When to use S3 versioning
Versioning is a good fit when accidental overwrite or deletion is a realistic operational risk and you want a fast, low-friction recovery path within the same bucket:
Application config files. Config or environment files that are deployed frequently. A bad push is immediately recoverable by restoring the previous version rather than rebuilding from scratch or digging through deployment logs.
Terraform state files. If you store
.tfstatein S3, versioning gives you a full history of every state change, making it possible to recover from an accidentalterraform destroyor a corrupted state file.User-uploaded assets. Images, documents, or media files that users can replace. Keeps the previous copy available if a replacement upload turns out to be wrong or corrupted.
Static website assets. CSS, JavaScript, and HTML files deployed by overwriting. You can roll back a broken deploy in seconds by restoring the previous version rather than re-running a pipeline.
Shared team buckets. Buckets written to by multiple people or services where accidental overwrites are a known risk. Versioning provides both a recovery path and an implicit audit trail of what changed.
Important structured logs. Log files rotated or rewritten by applications. If a bug causes logs to be zeroed or overwritten with garbage, the previous intact copy is still retrievable.
When versioning is not enough
Versioning is within-bucket, object-level protection. It handles accidental overwrites and deletions well, but it does not cover everything:
Malicious deletion by a credentialed actor. An attacker with valid AWS credentials can enumerate all versions with
list-object-versionsand delete each one by version ID. MFA Delete adds friction but does not make permanent deletion impossible for the root account. For true immutability against malicious deletion, use S3 Object Lock in Compliance mode, which blocks deletion even by account administrators.Disaster recovery and geographic resilience. All versions live in the same bucket, in the same AWS region. If the region has an outage, all versions are equally unavailable. For resilience against regional failure, you need cross-region replication or a separate backup strategy. See Disaster Recovery Strategies on AWS for how to plan for regional failure scenarios.
Compliance and legal retention. Versioning does not prevent an authorised IAM principal from deleting versions, and it does not enforce a minimum retention period. For records that must be preserved for a defined period, use S3 Object Lock with a retention period in Governance or Compliance mode.
Long-term backup isolation. All versions are accessible via the same credentials as the bucket. A compromised admin account can delete everything. True backup means a copy in a separate AWS account with separate IAM policies and no access path from the source environment.
If a principal with sufficient permissions runs delete-object —version-id on
every version, or a misconfigured lifecycle rule removes all objects, versioning provides no
protection. For genuine backup, copy data to a separate bucket in a separate AWS account
with separate IAM controls, not just a different bucket in the same account.
S3 Versioning vs Object Lock vs Backups
These three controls are complementary, not interchangeable. Each solves a different problem:
| S3 Versioning | S3 Object Lock | Cross-region backup | |
|---|---|---|---|
| Accidental deletion | Recoverable via delete marker | Blocked if object is locked | Recoverable from copy |
| Accidental overwrite | Recoverable via noncurrent version | Blocked if locked | Recoverable from copy |
| Malicious deletion (credentialed) | Vulnerable: versions deletable by version ID | Compliance mode: blocked even by admin | Depends on access isolation in target account |
| Immutability | No: any authorised user can delete versions | Yes: Compliance mode is unoverridable | No: copy can also be deleted |
| Geographic resilience | No: all versions in same region | No | Yes: copy lives in a separate region |
| Recovery speed | Fast: restore in seconds by version ID | Not applicable (cannot delete) | Slower: requires a copy-back operation |
| Cost / complexity | Low to moderate; grows without lifecycle rules | Low add-on (requires versioning) | Higher: storage, transfer, and orchestration |
For most applications, the right combination is versioning for day-to-day overwrite protection, lifecycle rules to control cost growth, and a separate backup copy for scenarios versioning cannot cover. Add Object Lock for regulatory or compliance requirements where immutability must be enforced.
Storage costs and lifecycle rules
Every version is stored separately and billed at the same S3 storage rate as the current version. A 500 MB object uploaded 20 times means up to 10 GB of billed storage unless lifecycle rules clean up noncurrent versions. For frequently updated objects (config files, Terraform state, application builds) costs accumulate faster than most teams expect.
The solution is a lifecycle policy with NoncurrentVersionExpiration. Apply it to
the same bucket at the same time you enable versioning. Here is a policy that expires
noncurrent versions after 30 days and cleans up lingering delete markers:
{
"Rules": [
{
"ID": "expire-noncurrent-versions",
"Status": "Enabled",
"Filter": {},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 30
},
"Expiration": {
"ExpiredObjectDeleteMarker": true
}
}
]
}Apply this policy via the AWS CLI:
aws s3api put-bucket-lifecycle-configuration \
--bucket my-company-assets \
--lifecycle-configuration file://lifecycle.jsonNoncurrentVersionExpiration permanently removes noncurrent versions after 30 days.
ExpiredObjectDeleteMarker removes delete markers that have no other versions
underneath them. Without it, orphaned delete markers accumulate silently and appear as
ghost entries in version listings.
Adjust the 30-day window to match your recovery requirements. If you only need to recover from mistakes made in the past 7 days, use 7. For critical data where longer history matters, 90 days is common. The S3 Lifecycle Policies guide covers the full rule syntax, including count-based retention (keep only the last N versions) and transitions to cheaper storage classes for older versions. For a broader picture of S3 cost drivers and how to measure them, see S3 Cost Optimisation.
Common mistakes
Enabling versioning without lifecycle rules. Every overwrite silently creates a new billable copy. On a bucket with frequent updates, storage costs can be several times higher than expected within weeks. Always configure
NoncurrentVersionExpirationat the same time as you enable versioning, not after you notice the bill.Assuming delete equals permanent deletion. On a versioned bucket,
aws s3 rmwithout a version ID creates a delete marker. The object disappears from normal listings but all previous versions remain stored and billed. To permanently remove an object and all its history, you must delete each version by its version ID.Misunderstanding what suspension does. Suspending versioning stops new versioned copies from being created but does not remove any existing versions. Storage charges continue for all noncurrent versions already present. Suspension is not a cleanup operation. It is a pause. Run
list-object-versionsafter suspending to audit what remains.Treating versioning as a full backup strategy. Versioning keeps copies within the same bucket, accessible via the same credentials. An attacker or runaway process with delete permissions can remove all versions. A compromised admin account can delete the bucket entirely. True backup requires a copy in a separate account with separate IAM controls.
Enabling MFA Delete without a root MFA recovery plan. If you enable MFA Delete and later lose access to the root MFA device, permanently deleting versions or changing versioning state will be blocked until you work through AWS Support. Confirm your root account MFA recovery and break-glass procedure before enabling MFA Delete on any production bucket.
Summary
- Versioning is disabled by default; enable it at the bucket level to preserve all copies of every object on overwrites and deletes.
- There are three bucket states: unversioned (default), versioning-enabled, and versioning-suspended. A bucket cannot return to unversioned once versioning has been enabled.
- Objects that existed before versioning was enabled, and new uploads while versioning is suspended, receive a null version ID rather than a real one.
- Deleting without specifying a version ID creates a delete marker. The previous content survives as noncurrent versions. To restore, delete the delete marker by its version ID.
- Permanently deleting a specific version requires its version ID. This is irreversible.
- MFA Delete can only be enabled by the root account and requires a valid MFA code to permanently delete versions or change versioning state.
- Every version is billed separately at the same rate as the current object. Always pair versioning with
NoncurrentVersionExpirationlifecycle rules. - Versioning protects against accidental overwrite and deletion. It does not provide immutability, geographic resilience, or isolation from a compromised admin account.
Frequently asked questions
Can I turn versioning off completely after enabling it?
No. Once versioning is enabled on a bucket, you can suspend it but you cannot revert to the unversioned state. Suspension stops new uploads from creating versioned copies, but all existing versions remain in storage and continue to incur charges. To reclaim storage, delete noncurrent versions manually or via a lifecycle rule.
What happens to objects that existed before I enabled versioning?
They receive a null version ID. They behave as the current version for that key. When you overwrite one after versioning is enabled, the null version becomes noncurrent and the new upload receives a real version ID. You can address and delete a null version by specifying --version-id null in CLI commands.
How do I restore a deleted object?
When versioning is enabled, deleting without specifying a version ID creates a delete marker rather than removing data. To restore the object, use list-object-versions to find the delete marker version ID, then delete the marker with delete-object --version-id. Once the marker is removed, the most recent actual version becomes current and the object reappears in normal listings.
Does versioning protect against ransomware or malicious deletion?
Versioning helps but is not a complete defence. An attacker with valid AWS credentials can enumerate all versions and delete each one by version ID, permanently removing everything. MFA Delete adds friction by requiring a valid MFA code for permanent version deletion, but only the root account can enable it. For true immutability against malicious deletion, use S3 Object Lock in Compliance mode.
How much can versioning increase my storage costs?
Every version is stored separately and billed at the same rate as the current object. A 500 MB config file uploaded 20 times means up to 10 GB of billed storage unless lifecycle rules clean up noncurrent versions. For high-frequency updates, this can multiply your bill significantly. Always pair versioning with NoncurrentVersionExpiration lifecycle rules.