Deleting objects
Delete one object, a specific version, or up to 1000 objects in a batch with Lockwell, and understand how delete markers interact with versioning.
Delete one object, a specific version, or up to 1000 objects in a single batch request. On a versioned bucket a delete usually does not remove bytes. It writes a delete marker that hides the object while its prior versions stay recoverable.
This page covers single deletes, batch deletes with per-key results, and how delete interacts with versioning. For the operation matrix, see the native data-plane reference and the S3 operations reference.
Delete one object
DeleteObject removes the current object (on an unversioned bucket) or writes a delete marker (on a versioned one). A
delete blocked by Object Lock retention or a legal hold fails with S3 403 AccessDenied; the native API returns 412
with retention_blocked or legal_hold_blocked. The native client reports whether a delete marker was created.
const res = await native.deleteObject("reports", "q1/summary.txt");
console.log(res.deleteMarker, res.versionId);The same with the S3 client:
await s3.deleteObject("reports", "q1/summary.txt");A delete is idempotent: deleting a key that is already gone succeeds. Deleting a missing key does not raise a not-found error.
Delete a specific version
Pass a versionId to delete one exact version rather than the current object. On a versioned bucket this is a hard
delete of that version (it does not create a delete marker), so use it to prune history or to remove a delete marker and
restore the prior version.
await native.deleteObject("reports", "q1/summary.txt", { versionId });await s3.deleteObject("reports", "q1/summary.txt", { versionId });Deleting the version id of a delete marker removes the marker, so the prior version becomes current again. List versions first to find the ids; see versioning.
Batch delete
DeleteObjects removes up to 1000 objects in one request. The batch may partially succeed: the server reports
per-key successes and per-key failures separately, so a single denied or retention-locked key does not fail the whole
call. The SDKs reject an over-1000 batch locally with a clear error before any request goes out.
A batch delete can partially succeed without raising an error. Always walk the errors list to see which keys
failed; the call itself does not throw on a per-key denial.
const res = await native.batchDeleteObjects("reports", ["q1/a.txt", { key: "q1/b.txt", versionId }]);
for (const d of res.deleted) console.log("deleted", d.key, d.deleteMarker);
for (const e of res.errors) console.log("failed", e.key, e.code, e.message);The S3 batch entries are a key string or { key, versionId }:
const res = await s3.deleteObjects("reports", ["q1/a.txt", { key: "q1/b.txt", versionId }]);
for (const d of res.deleted) console.log("deleted", d.key, d.deleteMarker);
for (const e of res.errors) console.log("failed", e.key, e.code, e.message);Quiet mode (S3 client)
By default the S3 batch response lists every successfully deleted key. Quiet mode suppresses those success entries and returns only the failures, which keeps the response small when you are deleting thousands of keys and only care about what went wrong. The server still deletes every key; only the response shrinks. Errors are always returned regardless of quiet mode.
const res = await s3.deleteObjects("reports", keys, { quiet: true });
// res.deleted is empty; res.errors holds any per-key failures.Per-key results
Each successful entry reports the key, and on a versioned bucket whether the delete created a delete marker plus that marker's version id. Each failed entry reports the key, an error code, and a message. Walk both lists after a batch:
const res = await native.batchDeleteObjects("reports", keys);
if (res.errors.length > 0) {
// Retry or surface the failures; the rest were deleted.
for (const e of res.errors) console.error(`${e.key}: ${e.code} ${e.message}`);
}A retention-locked or legal-held key shows up in errors (forbidden), not as a thrown call error, so one protected
object never blocks the rest of the batch.
Delete and versioning
What a delete does depends on the bucket's versioning state:
| Bucket state | DeleteObject (no version id) | DeleteObject (with version id) |
|---|---|---|
| Versioning disabled | Removes the object's bytes. | Removes that version (the only one). |
| Versioning enabled | Writes a delete marker; prior versions stay recoverable. | Hard-deletes that exact version. |
| Versioning suspended | Writes a null-version delete marker; existing versions stay. | Hard-deletes that version. |
After a delete-marker delete, a plain GetObject returns not-found (the marker hides the object), but the prior version
is still readable by its version id and recoverable by deleting the marker. To wipe an object and all its history,
delete every version id (list versions, then batch-delete with version ids).
// Wipe a key and all its versions on a versioned bucket:
const ids = [];
for await (const page of native.paginateObjectVersions("reports", { prefix: "q1/summary.txt" })) {
for (const v of page.versions) ids.push({ key: v.key, versionId: v.versionId });
}
await native.batchDeleteObjects("reports", ids);// Same wipe with the S3 client's version paginator:
const ids = [];
for await (const page of s3.paginateObjectVersions("reports", { prefix: "q1/summary.txt" })) {
for (const v of page.versions) ids.push({ key: v.key, versionId: v.versionId });
}
await s3.deleteObjects("reports", ids);Deleting buckets
DeleteBucket removes an empty bucket. The server rejects a delete of a bucket that still holds objects (or, on a
versioned bucket, versions or delete markers), so empty it first with a listing-driven batch delete. Bucket lifecycle is
covered in Versioning and the SDK references.
Next steps
- Versioning: list versions and delete markers, restore a deleted object.
- Listing & pagination: gather the keys to batch-delete.
- Object lock: why a retention-locked object refuses deletion.
Upload & download
Write and read object bytes with the Lockwell native and S3 clients, including buffered and streaming PutObject, GetObject, ranges, versions, and HeadObject.
Deployment
Deploy Lockwell as one container with one volume via Docker Compose, with two listeners, an at-rest master key, TLS at the proxy, and online backups.