Skip to content
AWS aws storage 5 min read

S3 Versioning

By default, when you upload an object to Amazon S3 (Simple Storage Service, AWS’s object storage) using a key that already exists, the new upload overwrites the old one and the previous data is gone forever. S3 versioning changes that: instead of replacing an object, S3 keeps the old copy and stores the new one alongside it. Each copy gets a unique version ID, so you can list, download, or restore any version you have ever stored. This is one of the simplest and most effective ways to protect a bucket against accidental overwrites, bad deploys, and even deletions.

How versioning works

When versioning is enabled on a bucket, every PUT creates a new version of the object rather than replacing the current one. The most recently uploaded version is called the current version; everything older is a noncurrent version. All of them stay in the bucket and remain individually retrievable by their version ID.

Deletes behave in a way that surprises a lot of people. When you delete an object in a versioned bucket without specifying a version, S3 does not erase any data. Instead it adds a small delete marker that becomes the new current version. A GET on the key then returns a 404 Not Found, so the object looks gone, but all the real versions are still there underneath the marker. To recover the object you simply remove the delete marker. To permanently erase data you must delete a specific version ID.

Versioning states

A bucket is always in exactly one of three states.

StateWhat it meansCan you go back?
UnversionedThe default. Overwrites and deletes are permanent.This is the starting point.
EnabledEvery version is kept; deletes add a delete marker.Yes, you can suspend.
SuspendedNew uploads stop getting version IDs (they get null), but existing versions are kept.Yes, you can re-enable.

Gotcha: Once you enable versioning you can never turn it fully off. You can only suspend it. Suspending stops new versions from being created but keeps every version you already have. The bucket is changed for life, so plan your cleanup strategy before you flip the switch.

When to use it (and when not to)

Use versioning when the data is important and an accidental overwrite or delete would hurt: source artifacts, customer uploads, configuration files, Terraform state, database backups, anything compliance-related. It is also a prerequisite for S3 cross-region replication, so if you ever want to replicate a bucket you will need versioning anyway.

Think twice before enabling it on buckets with very high write or churn rates, such as temporary scratch space, log staging, or caches that are rewritten constantly. On those, every overwrite creates a new stored version that you keep paying for, and the bill can grow silently. If you do enable it on such a bucket, you must pair it with a lifecycle rule (covered below).

Enabling versioning

AWS Management Console

  1. Open the S3 console and click the bucket name, for example my-app-data.
  2. Go to the Properties tab.
  3. Find the Bucket Versioning card and click Edit.
  4. Select Enable and click Save changes.

AWS CLI

aws s3api put-bucket-versioning \
  --bucket my-app-data \
  --versioning-configuration Status=Enabled

Confirm the state:

aws s3api get-bucket-versioning --bucket my-app-data

Output:

{
    "Status": "Enabled"
}

To suspend it later (this keeps existing versions, it does not delete them):

aws s3api put-bucket-versioning \
  --bucket my-app-data \
  --versioning-configuration Status=Suspended

Listing and restoring a prior version

First, list every version of the objects under a key prefix. The --prefix filter keeps the output small.

aws s3api list-object-versions \
  --bucket my-app-data \
  --prefix config/app.json

Output:

{
    "Versions": [
        {
            "Key": "config/app.json",
            "VersionId": "3sL4kqtJlcpXroDTDmJ.rWQTSWxQqXpW",
            "IsLatest": true,
            "Size": 482,
            "LastModified": "2026-06-15T09:14:22+00:00"
        },
        {
            "Key": "config/app.json",
            "VersionId": "1pT9kqtJlcpXroDTDmJ.aB2CdEfGhIjK",
            "IsLatest": false,
            "Size": 461,
            "LastModified": "2026-06-12T17:02:08+00:00"
        }
    ]
}

To restore an older version, download it by its version ID and re-upload it as the new current version. Re-uploading (rather than copying the ID forward) is the simplest reliable approach.

# Download the older version to a local file
aws s3api get-object \
  --bucket my-app-data \
  --key config/app.json \
  --version-id 1pT9kqtJlcpXroDTDmJ.aB2CdEfGhIjK \
  app.json.restored

# Upload it back as the new current version
aws s3 cp app.json.restored s3://my-app-data/config/app.json

Recovering a deleted object

If an object was deleted and is now hidden behind a delete marker, find the marker and delete it (not the data) to bring the object back.

aws s3api list-object-versions \
  --bucket my-app-data \
  --prefix config/app.json \
  --query "DeleteMarkers[?IsLatest].VersionId" \
  --output text
aws s3api delete-object \
  --bucket my-app-data \
  --key config/app.json \
  --version-id <DeleteMarkerVersionId>

Removing the delete marker makes the most recent real version the current one again. The object is back.

Controlling cost with lifecycle rules

This is the part tutorials skip. Every noncurrent version is billed at full storage rates for its storage class. A frequently overwritten 10 MB file rewritten 100 times a day produces about 1 GB of extra noncurrent data per day, and you keep paying for all of it indefinitely. At S3 Standard pricing (about $0.023 per GB-month in us-east-1 as of 2026), that is roughly $0.70/month per such file, growing forever.

The fix is a lifecycle rule that expires noncurrent versions after a set number of days. Save this as lifecycle.json:

{
  "Rules": [
    {
      "ID": "expire-old-versions",
      "Status": "Enabled",
      "Filter": { "Prefix": "" },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 30 },
      "Expiration": { "ExpiredObjectDeleteMarker": true }
    }
  ]
}
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-app-data \
  --lifecycle-configuration file://lifecycle.json

This deletes noncurrent versions 30 days after they become noncurrent and cleans up orphaned delete markers automatically. You keep a 30-day safety net without the bill ballooning.

Cost tip: Enabling versioning without a lifecycle rule is the single most common cause of mystery S3 bills. Add the noncurrent-version expiration rule on the same day you enable versioning, every time.

Best Practices

  • Enable versioning on any bucket holding data you cannot afford to lose, and treat it as the baseline for important buckets.
  • Always attach a lifecycle rule to expire noncurrent versions (30 to 90 days is a sensible default) the moment you enable versioning.
  • Add ExpiredObjectDeleteMarker: true to the lifecycle rule so leftover delete markers are cleaned up automatically.
  • Use MFA Delete (multi-factor authentication on deletes) on high-value buckets so no one can permanently delete versions without a hardware or software token.
  • Remember that suspending versioning does not remove existing versions, you still need a lifecycle rule or manual cleanup to reclaim that storage.
  • Test your restore procedure before you need it, so recovering a deleted object during an incident is routine rather than risky.
Last updated June 15, 2026
Was this helpful?