Object Storage - Conditional writes

Vedi come Markdown

Find out how to use If-Match and If-None-Match conditional headers to prevent overwrites and avoid race conditions on your OVHcloud Object Storage buckets

Objective

This guide explains how to use the If-Match and If-None-Match HTTP conditional headers on PutObject, DeleteObject, and CompleteMultipartUpload requests on your OVHcloud Object Storage buckets.

Requirements

Instructions

Why use conditional writes?

Object Storage is inherently concurrent - multiple application instances, microservices, or background jobs may read and write the same object at the same time. Without coordination, concurrent writes can silently overwrite each other, causing data loss that is difficult to detect and reproduce.

Conditional writes solve this problem at the storage layer, without requiring an external coordination service such as a database or a distributed lock manager. By attaching a precondition to an S31 API call, you ensure the operation only proceeds if the object is in the expected state at the moment the request is evaluated.

Key benefits:

  • Data integrity in concurrent environments: Prevent a write from silently overwriting changes made by another process between your read and your write (a classic check-then-act race condition).
  • Distributed locking primitive: Implement a lightweight, storage-native lock using If-None-Match: * on PutObject - only the first writer succeeds; all others receive 412 Precondition Failed.
  • Atomic compare-and-swap: Read an object's ETag, modify it, then write back with If-Match: <original_etag>. The write is rejected if the object was modified in between, giving you a safe optimistic concurrency pattern.
  • No extra infrastructure: Coordination happens inside the Object Storage service itself - no additional databases, queues, or lock managers required.
  • Any S3-compatible client: The conditional headers are part of the HTTP standard (RFC 9110) and the S3 API - they work with AWS CLI, any S3 SDK, and raw HTTP calls.

Overview

Conditional requests allow you to attach a precondition to an S3 API call based on the current state of the target object. OVHcloud Object Storage evaluates the condition atomically before executing the operation. If the condition is not met, the request is rejected without modifying any data.

Two HTTP headers are supported, based on RFC 9110:

HeaderAccepted valueMeaning
If-MatchBare ETag value or *Execute only if the object's current ETag matches
If-None-Match* onlyExecute only if the object does not exist

Supported operations:

OperationIf-MatchIf-None-Match
PutObject
DeleteObject
CompleteMultipartUpload
Info

ETag format: When using If-Match, provide the ETag as a bare string without surrounding quotes - for example d41d8cd98f00b204e9800998ecf8427e, not "d41d8cd98f00b204e9800998ecf8427e".

To retrieve an object's current ETag, use aws s3api head-object --bucket <bucket_name> --key <object_key> and read the ETag field from the response, stripping the surrounding quotes.

Warning

You cannot send both If-Match and If-None-Match in the same request. Doing so returns an HTTP 400 Bad Request error.

Response codes

HTTP codeMeaning
200 OK / 204 No ContentCondition was satisfied - operation executed
404 Not FoundIf-Match condition: the target object does not exist (no current version, or current version is a delete marker)
412 Precondition FailedIf-Match condition: the object exists but its ETag does not match; or If-None-Match: * and the object already exists
409 ConditionalRequestConflictA concurrent operation conflicted - retry required

Versioning and delete markers

All conditions are evaluated against the current version of the object, regardless of whether versioning is enabled on the bucket.

Info

A delete marker in a versioned bucket is not considered an existing object:

  • If-Match (with an ETag or *) → 404 Not Found when the current version is a delete marker.
  • If-None-Match: *succeeds when the current version is a delete marker (treated as non-existent).
Info

Encryption support: Conditional writes are supported for unencrypted objects and objects encrypted with SSE-S3. Support for SSE-C encrypted objects will be available in a future release.

PutObject - conditional write

Add a precondition to an object upload to prevent accidental overwrites or to implement first-write-wins logic.

Use cases:

  • First-write-wins / distributed lock: Multiple processes race to create the same object. Only the first call with If-None-Match: * succeeds - all others receive 412.
  • Safe overwrite: Read an object, modify it, write it back with If-Match: <original_etag>. If another client modified the object in between, the write is rejected.
  • Idempotent initialisation: Create a configuration object without risk of overwriting an existing version.
Via AWS CLI
Via curl (S3 REST API)
# Write only if the object does not exist (first-write-wins)
aws s3api put-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --body <local_file_path> \
  --if-none-match "*"

# Write only if the current ETag matches (safe overwrite)
aws s3api put-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --body <local_file_path> \
  --if-match <current_etag>

# Write only if the object exists (any ETag)
aws s3api put-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --body <local_file_path> \
  --if-match "*"

Successful upload:

{
    "ETag": "d41d8cd98f00b204e9800998ecf8427e"
}

Rejected upload (condition not satisfied):

An error occurred (PreconditionFailed) when calling the PutObject operation:
At least one of the pre-conditions you specified did not hold
Versioning behaviour for PutObject
Bucket stateIf-None-Match: *If-Match: <etag>If-Match: *
Non-versionedSucceeds only if no object existsSucceeds only if current ETag matchesSucceeds only if object exists
Versioned - current version exists412 Precondition FailedSucceeds if ETag matches → creates new versionSucceeds → creates new version
Versioned - current version is a delete markerSucceeds → creates new version404 Not Found404 Not Found
Versioned - no version at allSucceeds → creates first version404 Not Found404 Not Found

DeleteObject - conditional delete

Delete an object only if the ETag condition is satisfied. Only If-Match is supported for DeleteObject - If-None-Match is not applicable.

Use cases:

  • Safe delete: Read an object's ETag, then delete it with If-Match: <etag>. The delete is rejected if the object was modified between your read and your delete.
  • Delete-if-exists: Use If-Match: * to delete an object only if it currently exists.
Via AWS CLI
# Delete only if the current ETag matches
aws s3api delete-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --if-match <current_etag>

# Delete only if the object exists (any ETag)
aws s3api delete-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --if-match "*"

# Delete a specific version only if its ETag matches
aws s3api delete-object \
  --bucket <bucket_name> \
  --key <object_key> \
  --version-id <version_id> \
  --if-match <version_etag>

When --version-id is specified, the ETag is evaluated against that specific version, not the current version.

Info

In a versioned bucket, a successful DeleteObject creates a delete marker as the new current version - it does not permanently delete the object. To permanently delete a specific version, include --version-id in the request.

Versioning behaviour for DeleteObject
Bucket stateIf-Match: <etag>If-Match: *
Non-versionedPermanently deletes if ETag matchesPermanently deletes if object exists
Versioned - current version existsCreates delete marker if current version ETag matchesCreates delete marker if current version exists
Versioned - current version is a delete marker404 Not Found404 Not Found

CompleteMultipartUpload - conditional finalisation

Attach a precondition to the final step of a multipart upload. The condition is evaluated at the time CompleteMultipartUpload is called, not when CreateMultipartUpload was initiated.

Use cases:

  • Atomic large-file replacement: Begin a multipart upload of a large replacement file. At finalisation, use If-Match: <etag> to ensure the target object has not been modified during the upload.
  • First-write-wins for large files: Use If-None-Match: * at finalisation so that only one concurrent multipart upload permanently creates the object.
Via AWS CLI
# Finalize only if the object does not yet exist
aws s3api complete-multipart-upload \
  --bucket <bucket_name> \
  --key <object_key> \
  --upload-id <upload_id> \
  --multipart-upload file://parts.json \
  --if-none-match "*"

# Finalize only if the current ETag matches
aws s3api complete-multipart-upload \
  --bucket <bucket_name> \
  --key <object_key> \
  --upload-id <upload_id> \
  --multipart-upload file://parts.json \
  --if-match <current_etag>
Warning

If you receive a 409 ConditionalRequestConflict error on CompleteMultipartUpload, you must restart the entire multipart upload sequence:

  1. Call AbortMultipartUpload to clean up the in-progress upload.
  2. Retrieve the current ETag with HeadObject.
  3. Call CreateMultipartUpload to start a new upload.
  4. Re-upload all parts with UploadPart.
  5. Retry CompleteMultipartUpload with the updated condition.
Versioning behaviour for CompleteMultipartUpload
Bucket stateIf-None-Match: *If-Match: <etag>If-Match: *
Non-versionedSucceeds only if no object exists at finalisation timeSucceeds only if current ETag matches at finalisation timeSucceeds only if object exists at finalisation time
Versioned - current version exists412 Precondition FailedSucceeds if ETag matches → creates new versionSucceeds → creates new version
Versioned - current version is a delete markerSucceeds → creates new version404 Not Found404 Not Found
Versioned - no version at allSucceeds → creates first version404 Not Found404 Not Found

Important considerations

  • Atomicity: The condition check and the operation execute as a single atomic unit. No concurrent operation can alter the object between the check and the write.
  • Single header per request: You cannot combine If-Match and If-None-Match in the same request - this results in 400 Bad Request.
  • 409 retry for PutObject / DeleteObject: On 409 ConditionalRequestConflict, re-fetch the object's current ETag with HeadObject and retry your request with the updated value.
  • No IAM changes required: No additional permissions are needed. The existing s3:PutObject and s3:DeleteObject permissions are sufficient.
  • Object Lock compatibility: Conditional headers are evaluated independently from Object Lock (WORM) rules. Both constraints apply.

Go further

Join our community of users.

1: S3 is a trademark of Amazon Technologies, Inc. OVHcloud's service is not sponsored by, endorsed by, or otherwise affiliated with Amazon Technologies, Inc.

Questa pagina ti è stata utile?