Conditional writes & idempotency
Gate Lockwell writes on an object's current state with If-None-Match and If-Match, and make retried writes replay-safe with idempotency keys.
A conditional write gates a write on the object's current state, so two writers racing the same key cannot silently clobber each other. An idempotency key makes a retried write safe to replay without writing twice.
This page covers both, where each lives across the clients, and the safe-retry patterns to reach for. The headline distinction:
- Conditional writes (
If-None-Match/If-Match) are a native-client feature. The nativeputObjecttakes them directly. The S3PutObjectdoes not, so use the native client or a conditional copy for an existing object. - Idempotency keys work on both clients.
Create-only: If-None-Match: "*"
If-None-Match: "*" writes only when the key is absent. If something is already there, the write fails with a 412
precondition error and nothing is overwritten. This is the atomic "create, do not clobber" primitive: two callers racing
to create the same key, exactly one wins. It is a native-client write, so it always shows below the surface toggle.
import { isNativePreconditionFailed } from "@kelphect/sdk";
try {
await native.putObject("reports", "q1/summary.txt", body, { ifNoneMatch: "*" });
// we created it
} catch (err) {
if (isNativePreconditionFailed(err)) {
// someone else created it first
} else throw err;
}Overwrite-only: If-Match: "<etag>"
If-Match: "<etag>" writes only when the current object's ETag matches the value you pass. If the object changed
since you last read it (its ETag differs), the write fails with 412 and your update is rejected. This is optimistic
concurrency: read, compute a new value from what you read, then write back conditioned on the ETag you started from. A
mismatch means someone else wrote in between, so re-read and retry. This too is a native-client write.
// Read, mutate, write-back guarded by the ETag we read:
const cur = await native.getObject("config", "settings.json");
const next = patch(cur.body);
try {
await native.putObject("config", "settings.json", next, { ifMatch: cur.etag });
} catch (err) {
if (isNativePreconditionFailed(err)) {
// lost the race; re-read and retry
} else throw err;
}The ETag is the object's content identity. headObject or any read returns it, and so does the PutObject result, so
you can chain writes without an extra round-trip.
Conditional copy (the S3 path to conditional writes)
The S3 PutObject supports create-only If-None-Match: *; overwrite-only If-Match remains native-only. The S3
client also exposes copy-source conditionals. Native copy adds destination preconditions evaluated atomically at commit:
requireAbsentcopies only when the destination is absent (create-only).requireMatchEtagcopies only when the destination's ETag matches (overwrite-only).
// Create-only via copy: never clobber an existing snapshot.
await native.copyObject("snapshots", "latest.json", {
sourceBucket: "live",
sourceKey: "state.json",
requireAbsent: true, // 412 if snapshots/latest.json already exists
});Copy-source conditionals (gate on the source instead of the destination) are on both clients. See copying objects.
Idempotency keys
An idempotency key makes a write replay-safe: a retry that carries the same key and the same payload returns the
original stored result instead of writing a second time. Reach for it when a network blip might make you re-send a
PutObject you are not sure landed.
It works on both clients, but the native client has one extra requirement.
Native: pair the key with a body checksum
The native putObject streams the body and never buffers it, so it cannot hash the payload to prove a replay is the
same bytes. You supply that proof: pair the idempotency key with a checksums entry. The server verifies the digest
before committing any bytes, and uses the key plus the verified digest to collapse duplicate writes.
A native idempotent `putObject` needs a body checksum. Without a `checksums` entry, the streaming write has
no integrity proof and the server cannot confirm a retry is the same payload.
import { sha256ChecksumBase64 } from "@kelphect/sdk";
const body = "invoice-payload";
await native.putObject("billing", "invoices/2026-001.json", body, {
idempotencyKey: "invoice-2026-001",
checksums: { sha256: await sha256ChecksumBase64(body) },
});Supported algorithms include sha256, sha1, crc32, and crc32c. See checksums for the full
set.
S3: the buffered PutObject
The S3 PutObject buffers the body and signs its exact SHA-256, so an idempotency key needs no extra checksum: the
signed body hash is the integrity proof. Pass WithIdempotencyKey / idempotencyKey and the SDK signs the header so it
cannot be stripped or altered in transit.
await s3.putObject("billing", "invoices/2026-001.json", Buffer.from(body), {
idempotencyKey: "invoice-2026-001",
});The S3 streaming upload (putObjectStream) does not accept an idempotency key: its trailing checksum is not known
when the write is reserved. Use the buffered PutObject when you need idempotency, or
multipart upload for a large object (complete-multipart accepts an idempotency key).
How idempotency interacts with retries
Every native client refreshes its token before expiry and re-mints once on a 401. The Java native client also retries
transient failures by default, but it only replays PUT or POST when the request carries an idempotency key. Carry an
idempotency key (with its checksum) so any client-side retry is collapsed server-side. Retry tuning is on
Errors & retries.
The S3 client's automatic retry policy is idempotency-aware. It always retries safe methods (GET, HEAD, DELETE), and it
retries a PutObject / POST only when the request carries an idempotency key and has a re-readable buffered body.
A streaming body is never auto-retried (the reader is already consumed). This is exactly why an idempotency key matters:
it is what lets the client safely replay a write after a transport error or a 5xx.
// Default policy: up to 3 attempts with backoff + jitter. A keyed PutObject is
// eligible for automatic retry; without the key it is attempted once.
s3, _ := lockwellsdk.New(endpoint, creds,
lockwellsdk.WithRetryPolicy(lockwellsdk.DefaultRetryPolicy()))Choosing the right guard
| You want to... | Use |
|---|---|
| Create a key only if it does not exist | native putObject with ifNoneMatch: "*", or a copy with requireAbsent |
| Overwrite only if unchanged since you read it | native putObject with ifMatch: "<etag>", or a copy with requireMatchEtag |
| Make a retried write replay safely | an idempotency key (native: pair with a checksum) |
| Copy only if the source still matches | copy-source ifMatch / ifNoneMatch (both clients) |
Next steps
- Upload & download: the write these conditions gate.
- Copying objects: destination and source conditionals on copy.
- Checksums: the body-integrity digests native idempotency needs.
- Errors & retries: the retry policy that replays keyed writes.
Concepts in plain words
Plain-words definitions of the handful of terms the Lockwell docs use, from tenants and scoped keys to surfaces, bearer tokens, and signed URLs.
Copying objects
Duplicate objects server-side with Lockwell CopyObject, including cross-bucket copies, source and destination conditionals, and large-object UploadPartCopy.