Some data must not be deleted or changed for a fixed time, whatever anyone with access later wants: trade records, audit logs, medical images, backups that must survive a compromised admin account. That property is called WORM, write once read many. Permissions alone do not give it to you, because whoever holds the most powerful role can change the permissions.
Cloud Storage provides WORM with four controls that stack: a bucket retention policy, Bucket Lock to make that policy permanent, Object Retention Lock for per-object retain-until dates, and object holds for retention that depends on a business event. This article explains how each one decides whether a delete is allowed, works through a seven-year records bucket, covers how retention interacts with versioning, lifecycle, soft delete and encryption keys, and lists the mistakes that cannot be undone. Read it before you lock anything.
The controls and what each one blocks
Each control answers one question about an object. Versioning and soft delete are listed too, because people mistake them for WORM.
| Control | Scope | What it blocks | Can it be relaxed? |
|---|---|---|---|
| Retention policy | Bucket, all objects | Delete or replace until the object's age exceeds the period | Yes while unlocked |
| Bucket Lock | Bucket policy | Removing or shortening the policy | Never; the period can only increase |
| Object Retention Lock | One object | Delete or replace before its retain-until time | Unlocked mode: with a special permission. Locked mode: never |
| Event-based hold | One object | Delete or replace while set; restarts the retention clock on release | Yes, by removing the hold |
| Temporary hold | One object | Delete or replace while set; no effect on the clock | Yes, by removing the hold |
| Object Versioning | Bucket | Nothing; keeps old versions after overwrite | Not WORM |
| Soft delete | Bucket | Nothing; keeps deleted objects restorable for a window | Not WORM |
Notice that every control blocks both delete and replace. In a bucket without Object Versioning, uploading a new object with the same name is treated exactly like deleting the old one. With versioning on, the write succeeds and the protected copy becomes a noncurrent version that stays retained.
Bucket retention policies
A retention policy is a single duration on the bucket, stored in seconds, up to 3,155,760,000 seconds (100 years). An object may be deleted or replaced only when its age exceeds that duration. Age is measured from the object's creation time, and each object exposes a read-only retention expiration time showing when it becomes deletable. The policy applies retroactively: setting it on a bucket that already holds data protects those existing objects too, including older versions already in a versioned bucket.
Setting and inspecting a policy with gcloud:
# 7 years = 7 x 365.25 x 86,400 = 220,903,200 seconds
gcloud storage buckets update gs://acme-trade-records --retention-period=220903200s
gcloud storage buckets describe gs://acme-trade-records \
--format="default(retention_policy)"
# While unlocked, the policy can still be removed:
gcloud storage buckets update gs://acme-trade-records --clear-retention-periodAn unlocked policy is a strong guardrail against accidents and buggy jobs, but anyone who can update the bucket can remove it. Treat it as a safety net, not as compliance.
Bucket Lock: making the policy permanent
Locking a retention policy makes it permanent for the life of the bucket:
gcloud storage buckets update gs://acme-trade-records --lock-retention-periodAfter this, nobody, including Google Cloud project owners, can remove the policy or shorten its period. You can still increase it. Three consequences follow, and all of them are deliberate:
- The bucket cannot be deleted until every object in it has met the retention period. A locked 10-year policy means the bucket exists for up to 10 years after its newest write.
- Cloud Storage places a lien on the project when a bucket has a locked policy, so the project cannot be deleted either. Plan which project holds locked buckets; do not put them in a project you expect to tear down.
- Storage costs are committed. Every byte you write stays billable for the full period. A mistaken upload of 50 TB into a 7-year locked bucket is a 7-year bill.
Bucket Lock operations require the Storage Admin role. Because locking is irreversible, run it as a separate, reviewed step after you have tested the unlocked policy, never as part of bucket creation in an automated pipeline.
Object Retention Lock: per-object dates
A bucket policy gives every object the same period. Object Retention Lock gives each object its own retain-until time, which fits data with different legal clocks in one bucket. It must be enabled on the bucket, and once enabled it cannot be disabled. New buckets enable it with a flag; existing buckets can only be enabled through the Google Cloud console:
gcloud storage buckets create gs://acme-contracts --location=US --enable-per-object-retention
gcloud storage objects update gs://acme-contracts/c-1042.pdf \
--retain-until=2031-06-30T00:00:00Z --retention-mode=Unlocked
gcloud storage objects describe gs://acme-contracts/c-1042.pdf \
--format="default(retention_settings)"There are two modes. Unlocked (called GOVERNANCE in the XML API) lets a principal with storage.objects.overrideUnlockedRetention shorten, change or clear the configuration, and the command must pass --override-unlocked-retention. Locked (COMPLIANCE in the XML API) can only be extended; the mode itself cannot be changed back. Setting retention requires storage.objects.setRetention. The maximum retain-until time is 100 years from now.
An object can be subject to both a bucket policy and its own configuration; it is kept until both are satisfied. If you know S3, these modes map closely to its governance and compliance modes; S3 Object Lock compares well.
Event-based and temporary holds
Holds are flags on an object, with no date. While any hold is set the object cannot be deleted or replaced, although its metadata can still be edited. There are two kinds, and they differ only in how they interact with the bucket retention clock.
Releasing an event-based hold resets the object's time in the bucket, so the retention period starts counting at release. That is how you express 'keep for 7 years after the account closes' when you do not know the closing date at upload. Turn on the bucket's default event-based hold and every new object arrives held. Releasing a temporary hold leaves the clock alone; use it for legal holds and investigations that freeze data without changing its schedule.
gcloud storage buckets update gs://acme-accounts --default-event-based-hold
gcloud storage objects update gs://acme-accounts/acct-77/statement-2026-09.pdf --no-event-based-hold
gcloud storage objects update gs://acme-accounts/acct-12/ledger.parquet --temporary-holdRestrictions to design around: holds cannot be managed through the XML API, are not supported in buckets that use hierarchical namespace, and an event-based hold cannot be placed on an object that has its own retention configuration, nor a retention configuration on an object with an event-based hold. Choose one mechanism per object.
Worked example: seven years after account closure
A broker must keep customer statements for seven years after account closure, and trade confirmations for seven years after creation. Statements are written monthly while the account is open. Design:
- Bucket
acme-trade-recordsfor confirmations: retention policy 220,903,200 seconds, tested unlocked for a month, then locked. - Bucket
acme-accountsfor statements: same locked policy, plus default event-based hold. Every statement lands held, so the seven-year clock has not started. - When the account-closure event fires, a small job lists the account's prefix and releases each hold. The clock now starts at release, so each statement becomes deletable seven years after closure.
- A lifecycle rule deletes objects older than the period. Lifecycle never deletes before retention is satisfied, so the rule is a cleanup tool, not a risk; see GCS lifecycle rules.
- Litigation freezes use temporary holds, released when counsel signs off, with no change to schedules.
The release job is the only code that touches retention, so keep it small, logged and idempotent:
from google.cloud import storage
def release_account(bucket_name: str, account_id: str) -> int:
client = storage.Client()
released = 0
for blob in client.list_blobs(bucket_name, prefix=f"{account_id}/"):
if blob.event_based_hold:
blob.event_based_hold = False
blob.patch() # metadata edit: allowed while held
released += 1
return releasedFor infrastructure as code, the Terraform google provider exposes retention_policy { retention_period, is_locked } and default_event_based_hold on google_storage_bucket; check the attribute names against your provider version. Setting is_locked = true locks on apply, irreversibly, so keep it out of modules that create test buckets.
How retention interacts with other features
- Versioning. A live version with unexpired retention can still be made noncurrent by a new write in a versioned bucket. The noncurrent version stays protected; retention guards versions, not names.
- Lifecycle. Delete actions wait until retention is met, so a lifecycle rule shorter than the period silently does nothing until then.
- Soft delete. Once an object clears retention and is deleted, soft delete keeps it restorable for its window: 7 days by default, configurable from 7 to 90 days, or 0 to disable. It is recovery, not retention.
- Encryption keys. A customer-managed key that protects objects under Locked retention cannot be destroyed before retention expires. Do not let key rotation automation assume otherwise.
- XML multipart uploads. Completing an upload fails if it would overwrite an object still under retention.
- Storage class. Retention multiplies class choice: an Archive object kept seven years pays Archive rates for seven years. See Cloud Storage classes.
Verifying and auditing retention
Retention you have not tested is retention you are guessing about. Before and after locking, prove the controls behave as designed with a disposable object in each bucket:
echo probe > probe.txt
gcloud storage cp probe.txt gs://acme-trade-records/_probe/probe.txt
# Expect the retention expiration time field to read about seven years out:
gcloud storage objects describe gs://acme-trade-records/_probe/probe.txt
# In a bucket without Object Versioning, expect both to be refused:
gcloud storage rm gs://acme-trade-records/_probe/probe.txt
gcloud storage cp probe.txt gs://acme-trade-records/_probe/probe.txtRun the same probe against a held object and an object with its own retain-until time, and keep the outputs as audit evidence. Remember the probe itself is now retained for the full period, so keep probes tiny and under one prefix.
Then watch the control plane. Cloud Audit Logs record bucket configuration changes such as policy updates and locks in Admin Activity logs; object-level operations may only appear once Data Access logs are enabled, so confirm which log captures each event in your project before relying on it. Alert on any change to a retention policy, any use of the override flag and any hold released outside the release job's service account. A quarterly report of held object counts and retention expiration dates by prefix catches forgotten holds and wrong periods while they are still cheap to fix.
Failure modes
- Locking the wrong period. A typo of 2,209,032,000 instead of 220,903,200 seconds is 70 years and cannot be shortened. Compute periods in code and assert the result in years before locking.
- Locked bucket in a disposable project. The lien blocks project deletion and the bucket outlives the project's purpose.
- Overwrite-in-place pipelines. In buckets without versioning, jobs that rewrite
latest.jsonstart failing with permission-style errors the moment a policy is set. Write unique object names. - Forgotten event-based holds. Default holds without a release job keep data, and cost, forever. Alert on held objects older than your longest expected event delay.
- Per-object retention left Unlocked. Anyone with the override permission can clear it, which may fail an audit. Restrict that permission to a break-glass group with IAM and log its use.
- Assuming soft delete or versioning is WORM. Both can be disabled or bypassed by an admin.
Trade-offs
Use an unlocked bucket policy for guardrails on important data, a locked policy only where regulation or ransomware resistance demands that no one can shorten retention, Object Retention Lock when objects in one bucket need different dates, and holds when the clock starts at a business event. The trade is flexibility for certainty: every locked control removes your own ability to fix mistakes, including costly ones. Keep locked data in dedicated buckets and projects, with small, simple naming and write paths.
What to do next
- Write down each data class, its legal clock (from creation or from an event) and its period in seconds.
- Create dedicated buckets per class in a long-lived project; enable per-object retention at creation if you might need it.
- Set retention policies unlocked and run your writers, lifecycle rules and release jobs against them for a few weeks.
- Restrict Storage Admin and the override-unlocked-retention permission to a small group and alert on their use.
- Peer-review the exact period, then lock with a separate, manual change.
- Add monitoring for held object counts, bucket size growth and failed overwrite attempts.
- Document the lien and key-destruction constraints for whoever owns the project and keys next.