This website uses cookies

Read our Privacy policy and Terms of use for more information.

What it actually is

Amazon S3 Glacier is not a service you create. It is three storage classes inside S3: Glacier Instant Retrieval, Glacier Flexible Retrieval, and Glacier Deep Archive. Same buckets, same keys, same S3 API. What changes is the price curve and the access contract. You pay less per gigabyte stored, and in exchange you pay more every time you touch the data and, for two of the three classes, you wait.

The mental model the docs bury: an archived object in Flexible Retrieval or Deep Archive is not "slow to read". It is not readable at all. A GET returns an error until you issue a restore request, which asks S3 to create a temporary copy of the object. You then read the copy, and the copy expires. Archive classes are a two-step access pattern, and any code, pipeline, or human that assumes a one-step GET will break on day one.

One naming trap: there is also a legacy standalone Amazon Glacier service with vaults and archive IDs. AWS's own getting-started guide steers new users to the S3 console and calls the legacy service recommended only for pre-integration users. Everything in this article is the S3 storage classes.

The model

Per the S3 documentation, the three classes share the durability and resiliency of S3 Standard. What differs is the access contract:

Class

Minimum storage duration

Retrieval

Minimum billable object size

Glacier Instant Retrieval

90 days

Milliseconds, no restore step

128 KB

Glacier Flexible Retrieval

90 days

Restore required: Expedited 1-5 minutes, Standard 3-5 hours, Bulk 5-12 hours

See pricing page

Glacier Deep Archive

180 days

Restore required: Standard within 12 hours, Bulk within 48 hours, no Expedited

See pricing page

Instant Retrieval behaves like Standard-IA with a lower storage price and a higher access price. You GET it like any object. It is the archive class for data you rarely read but must read fast when you do, such as medical images or user uploads older than a quarter.

Flexible Retrieval and Deep Archive are the true archive classes. The restore flow has four moving parts. The RestoreObject request names a tier and a number of Days. S3 stages a temporary copy and keeps it for that many days. HeadObject exposes the state through a Restore field: ongoing-request="true" while it runs, ongoing-request="false" with an expiry-date when the copy is ready. And the event s3:ObjectRestore:Completed tells you when to stop polling. While the copy exists you pay for both the archived object and the restored copy, which is a detail that matters when you restore a lot at once.

Two details about the plumbing. First, Expedited retrievals without provisioned capacity "might not be accepted during periods of high demand", per the docs. A provisioned capacity unit guarantees at least three Expedited retrievals every 5 minutes and up to 300 MB/s. Second, for many objects, S3 Batch Operations is the supported way to restore at scale: its Standard restores on Flexible Retrieval begin within minutes and finish in 3-5 hours, and for Deep Archive they typically finish within 9 hours.

Lifecycle transitions are a one-way waterfall. From Standard you can go to any Glacier class. Glacier Instant Retrieval can go to Flexible or Deep Archive. Flexible can go to Deep Archive only. Deep Archive goes nowhere. To move data back out, you restore and copy.

What AWS operates: durability, the staging of restored copies, the lifecycle engine. What you operate: the decision about when data is cold enough, the restore orchestration, and the arithmetic.

When to use it, when not to

Situation

Reach for

Why

Rarely read, but must be instant when it is

Glacier Instant Retrieval

No restore step, millisecond access

Backups you may restore once a year

Flexible Retrieval

Restore in hours, free Bulk tier

Seven-year compliance retention

Deep Archive

Lowest storage price, 180 day minimum fits retention

Access pattern unknown

S3 Intelligent-Tiering

It tiers for you, for a monitoring fee

Billions of tiny objects

Aggregate first, then archive

Per-object overhead and request fees dominate

The honest summary: archive classes are for data whose access pattern you can predict, and predicting it is the whole job. If you cannot say how often you will read it, Intelligent-Tiering costs you a monitoring fee to avoid being wrong. If you can say, Glacier is cheaper, and being wrong costs you in the retrieval column rather than the storage column.

What it costs

Cost has five dimensions, and only one of them is the headline number on the pricing page.

Storage per GB-month is the headline. As a rough orientation only, third-party summaries quote roughly 0.004 USD for Instant Retrieval, roughly 0.0036 USD for Flexible Retrieval and roughly 0.00099 USD for Deep Archive per GB-month in a US region. Do not budget from my numbers; take current figures for your region from the S3 pricing page. The script below takes prices as inputs for exactly that reason.

The surprises come from the other four. First, per-object overhead: for Flexible Retrieval and Deep Archive, S3 adds 40 KB of metadata per object, 8 KB billed at S3 Standard rates and 32 KB at the archive rate. For a 1 KB object you pay for 41 KB. Second, minimum duration: delete or transition early and you are billed for the remainder of 90 or 180 days. A lifecycle rule that moves Instant Retrieval to Deep Archive at day 20 still bills the Instant Retrieval minimum, as the lifecycle docs spell out. Third, transition requests: each object moved into Flexible or Deep Archive is a billed request, so archiving a hundred million small files can cost more than the storage saves. Fourth, retrieval: AWS's own knowledge-center example prices Flexible Retrieval Standard at 0.05 USD per 1,000 requests plus 0.01 USD per GB, Expedited at 10 USD per 1,000 requests plus 0.03 USD per GB, and Bulk as free. In the same example, retrieving 15 million objects totalling 100 TB cost 1,774 USD on Standard and 153,072 USD on Expedited. The request fee, not the data fee, was the bill.

That example is the single most useful number in this article. Retrieval cost is dominated by object count when objects are small. The fix is upstream: pack small files into larger archives (tar or zip by day or by prefix) before they ever reach Glacier.

Free tier reality: the S3 free tier is small and aimed at Standard. Do not plan on it for archive classes.

The limits that bite

The restore request rate is 1,000 transactions per second per the docs; above that you get a ThrottlingException. Large restores run at up to 1-2 PB per day per account, and a single object over 5 TB restores at up to 300 MB/s, so a 50 TB object can take around 48 hours. Both limits can be raised through AWS Support.

Lifecycle has its own sharp edge. Since September 2024, objects smaller than 128 KB do not transition by default, in any rule, unless you add an ObjectSizeGreaterThan or ObjectSizeLessThan filter. Existing configurations keep the old behavior until you edit them, and then switch to the new default. A team that edits an old rule and silently stops archiving small objects is a real failure mode. Also: lifecycle transitions are asynchronous, but billing at the destination rate starts when the rule is satisfied, not when the physical move happens.

Restores are not in-place. The temporary copy lands in Standard storage for the number of Days you asked for, so Days is a cost dial. Ask for 30 days when you need 2 and you pay for 28 extra days of a Standard copy.

Build it

We will build three things: a bucket with an archival lifecycle rule, an upload of test objects straight into Flexible Retrieval, and a restore script that issues a Bulk restore and records how long it actually takes. Then a cost model so you can run the numbers before archiving anything real.

Prerequisites: an AWS account, Python 3.10+, boto3 installed (pip install boto3), AWS credentials configured, and a Region of your choice (examples use us-east-1; set AWS_REGION to match). Expect the build to cost a fraction of a cent: three 1 MB objects in an archive class, a free Bulk restore, and a short-lived Standard copy. Because of the 90 day minimum, deleting at cleanup is still billed for the remainder of the minimum duration, which for 3 MB is still a trivial amount.

IAM permissions the calling identity needs: s3:CreateBucket, s3:PutBucketVersioning is optional, s3:PutLifecycleConfiguration, s3:GetLifecycleConfiguration, s3:PutObject, s3:GetObject, s3:RestoreObject, s3:ListBucket, s3:DeleteObject, and s3:DeleteBucket. Scope them to your bucket name in real use.

Step 1: create the bucket and an archival lifecycle rule. The rule moves objects under the prefix cold/ to Glacier Instant Retrieval after 30 days and to Deep Archive after 180 days. The size filter is explicit so small objects are not silently skipped.

import os, boto3
region = os.environ.get("AWS_REGION", "us-east-1")
bucket = os.environ["BUCKET"]          # must be globally unique
s3 = boto3.client("s3", region_name=region)
kwargs = {"Bucket": bucket}
if region != "us-east-1":
    kwargs["CreateBucketConfiguration"] = {"LocationConstraint": region}
s3.create_bucket(**kwargs)
s3.put_bucket_lifecycle_configuration(
    Bucket=bucket,
    LifecycleConfiguration={"Rules": [{
        "ID": "cold-tiering",
        "Status": "Enabled",
        "Filter": {"And": {
            "Prefix": "cold/",
            "ObjectSizeGreaterThan": 131072,   # 128 KB, explicit on purpose
        }},
        "Transitions": [
            {"Days": 30,  "StorageClass": "GLACIER_IR"},
            {"Days": 180, "StorageClass": "DEEP_ARCHIVE"},
        ],
    }]},
)
print("bucket + lifecycle ready")

Note the shape: with two filter conditions you must wrap them in And. The rule respects the 90 day minimum of Instant Retrieval because the Deep Archive transition is at day 180, well past day 120. If you set it to day 20 you would pay for the 90 day minimum anyway.

Step 2: upload test objects directly into Flexible Retrieval. In production the lifecycle rule does the moving; for a tutorial we cannot wait 30 days, so we write with the storage class set at upload time.

import os, boto3
bucket = os.environ["BUCKET"]
s3 = boto3.client("s3")
payload = os.urandom(1024 * 1024)   # 1 MB of random bytes
for i in range(3):
    s3.put_object(Bucket=bucket, Key=f"restore-test/obj-{i}.bin",
                  Body=payload, StorageClass="GLACIER")
print("uploaded 3 objects as GLACIER (Flexible Retrieval)")

Step 3: confirm the access contract is what the docs say. A GET on an archived object must fail.

import os, boto3
from botocore.exceptions import ClientError
bucket = os.environ["BUCKET"]
s3 = boto3.client("s3")
try:
    s3.get_object(Bucket=bucket, Key="restore-test/obj-0.bin")
except ClientError as e:
    print(e.response["Error"]["Code"])   # expect InvalidObjectState

Step 4: issue a Bulk restore and time it. Bulk is free for Flexible Retrieval and the documented window is 5-12 hours. The script writes a start timestamp to disk first, so you can stop it and resume later without losing the measurement. Days is 1 to keep the Standard copy cheap.

import os, time, json, boto3
from datetime import datetime, timezone
bucket = os.environ["BUCKET"]
key = "restore-test/obj-0.bin"
s3 = boto3.client("s3")
STATE = "restore_state.json"
if not os.path.exists(STATE):
    s3.restore_object(Bucket=bucket, Key=key, RestoreRequest={
        "Days": 1, "GlacierJobParameters": {"Tier": "Bulk"}})
    json.dump({"start": time.time()}, open(STATE, "w"))
    print("restore requested at", datetime.now(timezone.utc).isoformat())
start = json.load(open(STATE))["start"]
while True:
    r = s3.head_object(Bucket=bucket, Key=key).get("Restore", "")
    if 'ongoing-request="false"' in r:
        mins = (time.time() - start) / 60
        print(f"restored after {mins:.0f} minutes; {r}")
        break
    time.sleep(300)   # poll every 5 minutes

Run it, go do something else, run it again when you are back. For production, replace polling with an S3 event notification on s3:ObjectRestore:Completed delivered to SQS, SNS, or Lambda. If you call restore_object on an object that is already restored, you are billed a GET request and, if the restore is still running, you get a RestoreAlreadyInProgress error, which is why the script guards the first call with a state file.

Step 5: a cost model. This is the piece to run before you archive real data. Fill PRICES from the current pricing page for your region. The retrieval numbers below are the knowledge-center example for Flexible Retrieval Standard; the storage number is a placeholder you must replace.

def archive_cost(objects, avg_kb, months, restores_per_year, price):
    gb = objects * avg_kb / 1024 / 1024
    overhead_gb = objects * 40 / 1024 / 1024
    storage = (gb + overhead_gb) * price["storage_gb_month"] * months
    transitions = objects / 1000 * price["transition_per_1000"]
    restore = restores_per_year * (
        objects / 1000 * price["restore_req_per_1000"]
        + gb * price["restore_gb"])
    return {"storage": storage, "transitions": transitions, "restore_per_year": restore}
PRICES = {   # REPLACE from https://aws.amazon.com/s3/pricing/ for your Region
    "storage_gb_month": 0.0036,   # placeholder: verify
    "transition_per_1000": 0.0,   # placeholder: verify
    "restore_req_per_1000": 0.05, # Flexible Standard, KC example
    "restore_gb": 0.01,           # Flexible Standard, KC example
}
print(archive_cost(15_000_000, 7, 12, 1, PRICES))

Run it twice: once with 15 million 7 KB objects, once with the same 100 GB packed into 10,000 archives of 10 MB. The difference in the restore line is the lesson.

Verify it works

Step 3 must print InvalidObjectState. After Step 4 completes, head_object must show Restore as ongoing-request="false" with an expiry-date about one day out, and a get_object on the key must now succeed and return 1,048,576 bytes. Record the minutes Step 4 printed. The docs promise 5-12 hours for Bulk on Flexible Retrieval; your measured number is the real input to any restore runbook you write. Check the lifecycle rule with get_bucket_lifecycle_configuration and confirm the ObjectSizeGreaterThan value is present.

When it breaks

InvalidObjectState on GET means the object is archived or the restored copy expired. Issue a restore, wait, read. RestoreAlreadyInProgress means a restore for that key is already running; check the Restore header instead of retrying. ThrottlingException on a large restore loop means you are above the 1,000 requests per second restore rate; use Batch Operations, which is designed to use the account's full restore rate. Expedited requests being refused in a busy period is the documented behavior without provisioned capacity; buy a unit or fall back to Standard. A lifecycle rule that is not moving small objects is almost always the 128 KB default; add an explicit size filter. And a restore that "worked" but a bill that jumped is usually Days set too high or a restore loop repeating GET-priced requests on already-restored objects.

Cleanup

Delete the objects and the bucket, and confirm nothing is left. Early deletion is billed for the remaining minimum duration, which for three 1 MB objects is negligible.

import os, boto3
bucket = os.environ["BUCKET"]
s3 = boto3.client("s3")
for page in s3.get_paginator("list_object_versions").paginate(Bucket=bucket):
    for v in page.get("Versions", []) + page.get("DeleteMarkers", []):
        s3.delete_object(Bucket=bucket, Key=v["Key"], VersionId=v["VersionId"])
s3.delete_bucket_lifecycle(Bucket=bucket)
s3.delete_bucket(Bucket=bucket)
print("bucket gone")

Then remove restore_state.json, and check Cost Explorer filtered to S3 over the next day to confirm only the minimum-duration remainder appears.

Sources