Lifecycle Policies for GCS and Azure Blob
On Google Cloud Storage, a bucket lifecycle JSON with Delete and SetStorageClass actions keyed on age, matchesPrefix, daysSinceCustomTime, daysSinceNoncurrentTime and numNewerVersions covers upload cleanup and tiering; consider Autoclass instead of manual transitions when access is unpredictable. On Azure Blob Storage, a storage-account management policy with baseBlob actions (tierToCool, tierToCold, tierToArchive, delete) keyed on daysAfterModificationGreaterThan or daysAfterLastAccessTimeGreaterThan does the same, with prefixMatch filters per container. In both, account for minimum storage durations — 30, 90 and 365 days for Nearline, Coldline and Archive on GCS; 30, 90 and 180 for Cool, Cold and Archive on Azure — and remember that archive tiers on Azure need rehydration before reads.
The concepts are the same as on S3 — expire what is temporary, tier what is cold, clean up old versions — but the rule syntax, filters and class behaviours differ enough to cause surprises when you port a policy. This page belongs to cloud storage lifecycle rules in backend validation and cloud storage architecture. For the S3 versions, see setting up S3 lifecycle rules for temporary uploads and transitioning media to cheaper storage classes.
When to use this approach
- Your uploads land in Cloud Storage via signed URLs or resumable sessions or in Azure Blob via SAS tokens.
- You keep temporary, unscanned or staging objects that must be removed automatically.
- Media storage costs are growing and much of it is cold.
Prerequisites
- GCS:
roles/storage.adminon the bucket, andgcloud470+ (thegcloud storagecommands). - Azure: Contributor on the storage account, a general-purpose v2 or Blob Storage account, and Azure CLI 2.60+.
- A key layout separating temporary, original and derived objects by prefix.
- For Azure access-time rules: last access time tracking enabled on the account.
Equivalent classes across providers
Implementation: Google Cloud Storage
lifecycle.json, applied with gcloud storage buckets update gs://media-prod --lifecycle-file=lifecycle.json:
{
"rule": [
{
"action": { "type": "Delete" },
"condition": { "age": 1, "matchesPrefix": ["tmp/", "unscanned/"] }
},
{
"action": { "type": "AbortIncompleteMultipartUpload" },
"condition": { "age": 3 }
},
{
"action": { "type": "SetStorageClass", "storageClass": "NEARLINE" },
"condition": { "age": 30, "matchesPrefix": ["originals/"], "matchesStorageClass": ["STANDARD"] }
},
{
"action": { "type": "SetStorageClass", "storageClass": "COLDLINE" },
"condition": { "age": 120, "matchesPrefix": ["originals/"], "matchesStorageClass": ["NEARLINE"] }
},
{
"action": { "type": "Delete" },
"condition": { "isLive": false, "daysSinceNoncurrentTime": 30 }
},
{
"action": { "type": "Delete" },
"condition": { "daysSinceCustomTime": 0, "matchesPrefix": ["exports/"] }
}
]
}
Implementation: Azure Blob Storage
policy.json, applied with az storage account management-policy create --account-name mediaprod --resource-group media --policy @policy.json:
{
"rules": [
{
"name": "delete-temporary",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["uploads/tmp/", "uploads/unscanned/"] },
"actions": { "baseBlob": { "delete": { "daysAfterModificationGreaterThan": 1 } } }
}
},
{
"name": "tier-originals",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": { "blobTypes": ["blockBlob"], "prefixMatch": ["media/originals/"] },
"actions": {
"baseBlob": {
"tierToCool": { "daysAfterLastAccessTimeGreaterThan": 30 },
"tierToCold": { "daysAfterLastAccessTimeGreaterThan": 120 },
"enableAutoTierToHotFromCool": true
},
"version": { "delete": { "daysAfterCreationGreaterThan": 90 } }
}
}
}
]
}
Enable access tracking first: az storage account blob-service-properties update --account-name mediaprod --resource-group media --enable-last-access-tracking true.
Line-by-line on the decisions that matter
- GCS
ageversus AzuredaysAfterModification. Both count from object creation or last write. GCS evaluatesagein whole days since creation; Azure offers modification, creation and last-access variants. Last access is the better signal for media, because a popular old file should stay warm. matchesStorageClasson GCS transitions. Without it, a rule could try to move an object “down” to a class it is already past, or apply twice. Chaining Standard → Nearline → Coldline with explicit source classes makes the path unambiguous.- GCS
AbortIncompleteMultipartUpload. This applies to uploads made through the XML multipart API. JSON API resumable sessions expire on their own after a week and leave no parts behind, so the rule matters only if you use S3-compatible multipart uploads. daysSinceCustomTimefor application-driven expiry. Set an object’scustomTimewhen you want it to expire — for example, an export the user can download for seven days — and a rule withdaysSinceCustomTime: 0deletes it once that time passes. It moves the expiry decision into your app without per-object rules.- Azure
enableAutoTierToHotFromCool. When a cool blob is read, Azure moves it back to Hot automatically, so a revived file stops incurring cool-tier read charges. It only works with last-access-time conditions. - Version cleanup. Both providers keep old versions at full price when versioning is on. The GCS
isLive: falserule and Azure’sversion.deleteaction bound that cost.
Autoclass on GCS
Autoclass is enabled per bucket (gcloud storage buckets update gs://media-derived --enable-autoclass --autoclass-terminal-storage-class=ARCHIVE). It is the GCS counterpart to S3 Intelligent-Tiering, with one limitation that shapes bucket design: you cannot combine it with SetStorageClass rules on the same bucket. If originals and derivatives share a bucket, you must pick one strategy for both. Splitting them into two buckets — originals with manual rules, derivatives with Autoclass — is often the cleanest outcome, and the separate buckets also make IAM simpler.
Objects smaller than 128 KiB stay in Standard under Autoclass and are not charged the management fee, which neatly avoids the small-object problem that manual transitions have.
Azure access tiers in practice
Azure policies run roughly once a day and can take up to 48 hours to take effect after a change. Last access time is recorded with a 24-hour granularity to limit write overhead, so “30 days since last access” is approximate. For user-facing media, avoid tierToArchive unless you build rehydration into the product: an archived blob returns 409 BlobArchived on read, and rehydration at standard priority can take up to 15 hours.
Porting a policy from S3
Most teams arrive at GCS or Azure with an S3 policy in mind. Translate intent rather than syntax, rule by rule. An S3 expiration on a tmp/ prefix becomes a GCS Delete with age and matchesPrefix, or an Azure delete with daysAfterModificationGreaterThan and a prefixMatch that includes the container name. An S3 transition to Glacier Instant Retrieval maps best to GCS Coldline — both are online with a 90-day minimum — and to Azure Cold. S3 Intelligent-Tiering maps to GCS Autoclass; on Azure, last-access rules with automatic return to Hot are the nearest equivalent.
Some S3 features have no direct counterpart. Azure has no size filter in lifecycle rules, so small thumbnails in a tiered prefix will be moved too; keep them under a separate prefix that no tiering rule matches. GCS lifecycle conditions cannot filter on object tags, only on prefixes, suffixes, class, age, custom time and version state, so key layout carries all the weight. And both providers evaluate rules on their own schedule, so application logic that assumes an object is gone at exactly midnight on day seven must check expiry itself.
Write a short test for each rule before relying on it: upload an object under the prefix with a backdated custom time (GCS) or on a test account with a one-day age, wait for the next evaluation, and confirm the action happened. Lifecycle mistakes are silent — nothing errors when a rule matches nothing — and the first sign is usually a bill, or a missing file.
Configuration gotchas
GCS rule changes take up to 24 hours. New or changed lifecycle configurations can take a day to apply, and actions run asynchronously. Test on a separate bucket with small ages rather than waiting on production.
Azure policy rejected with a filter error. prefixMatch values include the container name (media/originals/), not just the path inside it. A prefix without the container matches nothing.
Early deletion charges after a policy change. Shortening a delete age for objects already in Nearline/Coldline or Cool/Cold bills the remaining minimum. Check the class of affected objects before tightening deletes.
Temporary objects vanish mid-processing. A one-day delete on unscanned/ assumes processing finishes within a day. If your scanner can back up, lengthen the age or move objects out of the prefix as soon as processing starts.
Verification
# GCS
gcloud storage buckets describe gs://media-prod --format='json(lifecycle_config)'
gcloud storage objects describe gs://media-prod/originals/2026/06/a.mov --format='value(storage_class)'
# Azure
az storage account management-policy show --account-name mediaprod --resource-group media --query policy.rules[].name
az storage blob show --account-name mediaprod --container-name media --name originals/2026/06/a.mov --query properties.blobTier
Frequently Asked Questions
Can one policy file work for both providers?
No; the schemas differ completely. Keep both in infrastructure code (Terraform has resources for each) with the same intent documented alongside, and review them together when the policy changes.
Do lifecycle transitions cost anything?
Yes. Both providers charge per-object operation fees for class changes, at the rate of the destination class’s write operations. Millions of small objects make this noticeable; filter by prefix to leave thumbnails alone.
What is the Azure equivalent of S3’s abort-incomplete-multipart rule?
Uncommitted blocks in Azure are discarded automatically after seven days, so no rule is needed.