GCP Error Guide: 'Error 412: At least one of the pre-conditions you specified did not hold' — Fix Precondition Failed
Fix GCP 412 'precondition did not hold' on Cloud Storage: align generation, metageneration, and etag preconditions, handle concurrent writes and stale requests.
- #gcp
- #cloud
- #troubleshooting
- #errors
Stuck on this GCP with AI error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Overview
Google Cloud returns 412 PRECONDITION_FAILED when a conditional request’s precondition — a generation, metageneration, or etag you asserted — no longer matches the resource’s current state. The literal Cloud Storage response reads:
{
"error": {
"code": 412,
"message": "At least one of the pre-conditions you specified did not hold.",
"status": "FAILED_PRECONDITION"
}
}
The gcloud/client form for an object write looks like:
PreconditionFailed: 412 At least one of the pre-conditions you specified did not hold.
This is not a permission or “not found” error — it is optimistic concurrency control working as designed. You told GCS “only apply this write if the object is still at generation X,” the object changed (someone else wrote it, or you’re using a stale value), and the server correctly refused. It is also the expected result of an x-goog-if-generation-match: 0 “create only if absent” guard when the object already exists.
Symptoms
412/FAILED_PRECONDITIONon object uploads, deletes, or metadata updates that set anifGenerationMatch/ifMetagenerationMatchprecondition.- Terraform or a client returning 412 during concurrent writes to the same object.
- “Create if not exists” logic (
ifGenerationMatch=0) failing because the object already exists. - Intermittent 412s under concurrency that succeed on retry with a refreshed generation.
- An
If-Match/etag conditional request to another Google API failing after the resource changed.
Common Root Causes
- Concurrent writers — two processes read the same generation, both try to write conditionally, and the second loses the race with a 412 (this is correct behavior).
- Stale generation/etag — the client cached a generation or etag and reused it after the resource changed.
ifGenerationMatch=0on an existing object — a “create only if absent” guard firing because the object is already there.- Retry without refreshing the precondition — retrying the exact same conditional request after it lost the race, instead of re-reading and recomputing.
- Wrong precondition semantics — using
ifMetagenerationMatchwhen you meantifGenerationMatch, or vice versa (content vs. metadata versioning). - Clock/order assumptions — assuming a read-then-write is atomic when another writer interleaves.
Diagnostic Workflow
Read the object’s current generation and metageneration — the values your precondition is being checked against:
gcloud storage objects describe gs://BUCKET/OBJECT \
--format='value(generation, metageneration, etag)'
Reproduce the conditional write and observe the 412 vs. the live generation:
# This fails with 412 if OBJECT already exists (create-only guard)
gcloud storage cp FILE gs://BUCKET/OBJECT --if-generation-match=0
Check for object versioning / concurrent writers on the bucket:
gcloud storage buckets describe gs://BUCKET --format='value(versioning.enabled)'
gcloud storage objects list gs://BUCKET/OBJECT --all-versions \
--format='table(name, generation, timeCreated)'
Confirm your client’s retry logic re-reads generation between attempts (inspect the code path emitting the precondition header).
Example Root Cause Analysis
A deploy pipeline wrote a shared state object to a bucket with if-generation-match to guard against concurrent updates. During parallel CI runs it intermittently failed with 412 precondition did not hold.
Diagnosis: gcloud storage objects describe showed the object’s generation advancing rapidly during parallel runs. Two CI jobs read the same generation, both attempted a conditional write, and the second consistently got a 412 — exactly what optimistic concurrency is supposed to do. The pipeline’s retry simply re-submitted the same stale generation, so it failed again.
Root cause: concurrent writers racing on one object; the 412 was correct, but the retry logic reused the stale generation instead of re-reading the current one before retrying.
Fix: implement a read-modify-write loop — on 412, re-read the object’s current generation, recompute the change, and retry the conditional write — with bounded exponential backoff. For truly serialized state, move to a proper locking mechanism rather than racing on one object. The intermittent failures resolved.
Prevention Best Practices
- Treat 412 as expected under concurrency: on failure, re-read the current generation/etag, reapply your change, and retry — never retry the same stale precondition.
- Use
ifGenerationMatch=0deliberately as a create-only guard, and handle the 412 it throws when the object already exists as a normal branch, not an error. - Pick the right precondition:
ifGenerationMatchfor object content,ifMetagenerationMatchfor metadata-only changes. - Add bounded exponential backoff with jitter to read-modify-write loops so racing writers don’t livelock.
- For genuinely serialized state, use a dedicated lock or a database with transactions instead of racing conditional writes on one object.
- Enable object versioning if you need to recover the generation a losing writer would have overwritten.
Quick Command Reference
# Show current generation/metageneration/etag
gcloud storage objects describe gs://BUCKET/OBJECT \
--format='value(generation, metageneration, etag)'
# Create-only guard (412 if it already exists)
gcloud storage cp FILE gs://BUCKET/OBJECT --if-generation-match=0
# Conditional overwrite at a known generation
gcloud storage cp FILE gs://BUCKET/OBJECT --if-generation-match=GENERATION
# Check versioning and list versions
gcloud storage buckets describe gs://BUCKET --format='value(versioning.enabled)'
gcloud storage objects list gs://BUCKET/OBJECT --all-versions
Conclusion
Error 412: at least one of the pre-conditions you specified did not hold is optimistic concurrency control doing its job, not a bug — your conditional request asserted a generation, metageneration, or etag that no longer matches the resource. The fix is almost never to remove the precondition (that reintroduces the race it was guarding against) but to handle it: on 412, re-read the current generation, reapply your change, and retry with backoff. Use ifGenerationMatch=0 intentionally for create-only writes, pick generation vs. metageneration correctly, and reach for real locking when you need true serialization.
Fixed it? Get 500 GCP with AI & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.