DeleteObjects
DeleteObjects removes up to 1,000 objects from one bucket in a single request, which saves a round trip per object when there is a lot to tidy away. Each entry names a key and, if you like, a version ID. The response reports how every entry went - or, in quiet mode, only the ones that did not go through. For a single object, DeleteObject does the same job.
Every STRG bucket keeps every version of its objects, so what an entry does depends on whether it carries a version ID:
| Entry | Effect |
|---|---|
| Key only | Writes a delete marker. The object disappears from listings and reads, but every earlier version is kept - and billed as stored data - until it is deleted by its version ID or a lifecycle policy removes it |
Key and VersionId | Permanently deletes that version. Naming a delete marker's version ID removes the marker, so the previous version becomes current again |
- For an access key limited to an object prefix, every key in the request is checked against the prefix up front, so a request either stays inside the prefix as a whole or deletes nothing.
- A key with a
.or..path segment (a/../b) cannot be written into a URL, and DeleteObjects is the way to delete one: its keys travel in the request body. - Per-object conditions (
ETag,LastModifiedTime,Size) andx-amz-mfaare answered with501 NotImplemented, so a client knows straight away. x-amz-expected-bucket-owneris welcome and has nothing to check: a key only ever reaches buckets of its own account.
When you'd use it
- Clearing out a batch of files. A photo app whose user deletes an album can remove all of its pictures in one request rather than one request per picture.
- Cleaning up after tests. A CI pipeline that writes objects under a run's own prefix can list them at the end and delete them in batches of 1,000, so test data does not pile up as stored data.
- Freeing up stored data for good. Because every bucket keeps old versions, a clean-up script can list them with ListObjectVersions and delete them by version ID, which is what stops them being billed.
- An agent tidying up after itself. An AI agent can delete the files it no longer needs - with a human confirming first whenever an entry names a version, unless its key allows it to act without confirmation.
Request
POST /?delete HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: <SHA-256 of the body>
Content-MD5: <base64 MD5 of the body>
Content-Type: application/xml
<Delete xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Object>
<Key>photos/2024/cat.jpg</Key>
</Object>
<Object>
<Key>photos/2024/dog.jpg</Key>
<VersionId>VersionId</VersionId>
</Object>
<Quiet>false</Quiet>
</Delete>
The path-style form is POST /my-bucket?delete on s3.eu-central-1.strg.com -
Addressing explains both forms.
Permissions
What the access key needs depends on the entries in the request - Access keys describes each permission.
| Entry | Permission |
|---|---|
| Key only (writes a delete marker) | Write objects (objects:write) |
Key and VersionId (permanent delete) | Delete object versions (objects:delete-versions) |
With x-amz-bypass-governance-retention: true | Also Bypass governance retention (object-retention:bypass-governance) |
A key limited to certain buckets reaches only those buckets. A key limited to an
object prefix is checked against every key in the
request. STRG checks all the entries before deleting anything, so a single entry the access
key may not delete - outside the prefix, or with a version ID when the key does not hold
Delete object versions - answers the whole request with 403 AccessDenied, and nothing is
deleted. A request is therefore all within the key's reach or left untouched.
URI parameters
| Name | Type | Required | Description |
|---|---|---|---|
Bucket | String | Yes | The bucket holding the objects. |
delete | Flag | Yes | Selects this operation. Sent without a value. |
Request headers
| Name | Type | Required | Description |
|---|---|---|---|
Content-MD5 | String | Yes, unless an x-amz-checksum-* header is sent | The base64-encoded MD5 digest of the request body. A request with neither is answered with 400 InvalidRequest. The AWS SDKs and the AWS CLI send an integrity header automatically, so this is taken care of. |
x-amz-checksum-crc32, x-amz-checksum-crc32c, x-amz-checksum-crc64nvme, x-amz-checksum-sha1, x-amz-checksum-sha256 | String | No | A checksum of the request body, accepted in place of Content-MD5. |
x-amz-sdk-checksum-algorithm | String | No | The algorithm the SDK used for the checksum header. |
x-amz-bypass-governance-retention | Boolean | No | true deletes object versions locked in governance mode. Needs Bypass governance retention. It has no effect on compliance mode - Bypassing governance retention has the details. |
x-amz-mfa | String | No | STRG has no MFA delete, and answers 501 NotImplemented. |
x-amz-request-payer | String | No | Has nothing to do: STRG has no Requester Pays buckets. |
x-amz-expected-bucket-owner | String | No | Welcome, and has nothing to check: an access key only ever reaches its own account's buckets. |
Request body
A Delete document of up to 2 MiB (2,097,152 bytes) - room for 1,000 keys of the longest
length. A larger body is answered with 400 MaxMessageLengthExceeded.
| Element | Type | Required | Description |
|---|---|---|---|
Delete | Container | Yes | The root element. |
Delete/Object | Container | Yes | One entry per object or version to delete. At least 1, at most 1,000. |
Delete/Object/Key | String | Yes | The object key. Not empty. |
Delete/Object/VersionId | String | No | The version to delete permanently. Without it, the entry writes a delete marker instead. |
Delete/Object/ETag | String | No | A conditional delete, which STRG answers with 501 NotImplemented. |
Delete/Object/LastModifiedTime | Timestamp | No | A conditional delete, which STRG answers with 501 NotImplemented. |
Delete/Object/Size | Long | No | A conditional delete, which STRG answers with 501 NotImplemented. |
Delete/Quiet | Boolean | No | true returns only the entries that failed. Defaults to false, which returns every entry. At most once. |
STRG reads the document strictly, so every key is read exactly the way it was meant.
Comments, CDATA sections, a DOCTYPE, processing instructions (other than a leading
<?xml …?> declaration), namespace prefixes, attributes other than xmlns, unknown elements
and entities other than XML's five predefined ones and character references are answered with
400 MalformedXML. The documents the AWS SDKs and the AWS CLI build are all welcome as they
are.
Response
A successful request answers with a DeleteResult document. The request succeeds as a whole even
when some entries do not, so the Error elements in the body are where an entry that failed
shows up.
Response headers
| Name | Description |
|---|---|
x-amz-request-id | The request ID, for support requests. |
x-amz-request-charged is never sent, since STRG has no Requester Pays buckets.
Response body
| Element | Description |
|---|---|
DeleteResult | The root element. |
DeleteResult/Deleted | One per entry that succeeded. Omitted in quiet mode. |
Deleted/Key | The object key. |
Deleted/VersionId | The version that was deleted, for an entry that named one. |
Deleted/DeleteMarker | true when a delete marker was written, or when the version deleted was itself a delete marker. |
Deleted/DeleteMarkerVersionId | The version ID of the delete marker written, for an entry without a version ID. |
DeleteResult/Error | One per entry that failed. |
Error/Key | The object key. |
Error/VersionId | The version ID named in the entry, if any. |
Error/Code | An S3 error code, such as AccessDenied. |
Error/Message | A description of the error. |
<?xml version="1.0" encoding="UTF-8"?>
<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<Deleted>
<Key>photos/2024/cat.jpg</Key>
<DeleteMarker>true</DeleteMarker>
<DeleteMarkerVersionId>DeleteMarkerVersionId</DeleteMarkerVersionId>
</Deleted>
<Deleted>
<Key>photos/2024/dog.jpg</Key>
<VersionId>VersionId</VersionId>
</Deleted>
</DeleteResult>
Errors
These errors answer the request as a whole, and nothing in it is deleted:
| Code | HTTP status | When |
|---|---|---|
MalformedXML | 400 | The body is not a valid Delete document, has no Object, more than 1,000, or an empty Key. |
MaxMessageLengthExceeded | 400 | The body is larger than 2 MiB. |
InvalidRequest | 400 | Neither Content-MD5 nor an x-amz-checksum-* header was sent. |
BadDigest | 400 | Content-MD5 does not match the body. |
XAmzContentSHA256Mismatch | 400 | x-amz-content-sha256 does not match the body. |
NotImplemented | 501 | The request carries x-amz-mfa, or an entry carries ETag, LastModifiedTime or Size. |
AccessDenied | 403 | The access key is not valid for the bucket, a key is outside its object prefix, or it lacks a permission an entry needs - see Permissions above. |
NoSuchBucket | 404 | There is no bucket of that name of yours in the endpoint's region - it does not exist, is in another region, or your account has nothing stored in this endpoint's region yet. |
InvalidAccessKeyId | 403 | The access key does not exist, has expired or been revoked, or it was created moments ago and is not yet usable in this endpoint's region, where you already store data. It becomes usable shortly after. |
SignatureDoesNotMatch | 403 | The request signature is wrong. |
RequestTimeTooSkewed | 403 | The request time is more than 15 minutes from the server's. |
AuthorizationHeaderMalformed | 400 | The signature's credential scope names a region other than the endpoint's. |
SlowDown | 503 | Too many requests at once. The AWS CLI and SDKs retry after the Retry-After interval. |
ServiceUnavailable | 503 | A temporary failure. The AWS CLI and SDKs retry it on their own. |
An entry can also fail on its own, while the others go through. It is then reported in an
Error element of the response, with its own code and message. A version under retention or a
legal hold, for example, stays until the lock ends - Object Lock explains
how locks work.
Differences from Amazon S3
| Aspect | Amazon S3 | STRG |
|---|---|---|
| Versioning | Deleting a key in an unversioned bucket removes the object | Every bucket is versioned: a key without a version ID writes a delete marker, and earlier versions stay billed as stored data |
| Access key limited to a prefix | Evaluated per object by the policy | Checked for every key before anything is deleted: a key outside the prefix answers the whole request with 403 AccessDenied |
| Permissions | s3:DeleteObject, s3:DeleteObjectVersion evaluated per object | A version ID in any entry needs Delete object versions, checked up front for the whole request |
Keys with . or .. segments | Addressable by every operation | DeleteObjects is the way to delete them, because the key travels in the body rather than the URL |
ETag, LastModifiedTime, Size in an entry | Conditional delete | 501 NotImplemented |
x-amz-mfa | MFA delete | 501 NotImplemented |
| XML parsing | Lenient | Strict, so every key is read as meant: comments, CDATA, DOCTYPE and namespace prefixes are 400 MalformedXML |
| Request body | - | Up to 2 MiB |
| Bucket in another region | 301 redirect with x-amz-bucket-region | 404 NoSuchBucket. The bucket's own regional endpoint reaches it. |
x-amz-expected-bucket-owner | Checked against the bucket owner | Welcome, with nothing to check |
Examples
Writing delete markers for two objects:
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api delete-objects \
--bucket my-bucket \
--delete '{"Objects": [{"Key": "photos/2024/cat.jpg"}, {"Key": "photos/2024/dog.jpg"}]}' \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
response = s3.delete_objects(
Bucket="my-bucket",
Delete={
"Objects": [
{"Key": "photos/2024/cat.jpg"},
{"Key": "photos/2024/dog.jpg"},
]
},
)
for deleted in response.get("Deleted", []):
print("deleted", deleted["Key"], deleted.get("DeleteMarkerVersionId"))
for error in response.get("Errors", []):
print("failed", error["Key"], error["Code"], error["Message"])
import { DeleteObjectsCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const { Deleted = [], Errors = [] } = await client.send(
new DeleteObjectsCommand({
Bucket: "my-bucket",
Delete: {
Objects: [{ Key: "photos/2024/cat.jpg" }, { Key: "photos/2024/dog.jpg" }],
},
}),
);
for (const deleted of Deleted) console.log("deleted", deleted.Key, deleted.DeleteMarkerVersionId);
for (const error of Errors) console.log("failed", error.Key, error.Code, error.Message);
Permanently deleting every version of photos/2024/cat.jpg, reporting only the entries that
fail. The version IDs come from ListObjectVersions, and the access
key needs Delete object versions.
A version deleted by its version ID is gone for good and cannot be restored.
- AWS CLI
- Python (boto3)
- JavaScript (AWS SDK v3)
aws s3api list-object-versions \
--bucket my-bucket \
--prefix photos/2024/cat.jpg \
--query '{Objects: [Versions[?Key==`"photos/2024/cat.jpg"`], DeleteMarkers[?Key==`"photos/2024/cat.jpg"`]][][].{Key: Key, VersionId: VersionId}, Quiet: `true`}' \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1 > delete.json
aws s3api delete-objects \
--bucket my-bucket \
--delete file://delete.json \
--endpoint-url https://s3.eu-central-1.strg.com \
--region eu-central-1
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://s3.eu-central-1.strg.com",
region_name="eu-central-1",
)
key = "photos/2024/cat.jpg"
listing = s3.list_object_versions(Bucket="my-bucket", Prefix=key)
objects = [
{"Key": entry["Key"], "VersionId": entry["VersionId"]}
for entry in listing.get("Versions", []) + listing.get("DeleteMarkers", [])
if entry["Key"] == key
]
response = s3.delete_objects(
Bucket="my-bucket",
Delete={"Objects": objects, "Quiet": True},
)
for error in response.get("Errors", []):
print("failed", error["Key"], error["VersionId"], error["Code"], error["Message"])
import { DeleteObjectsCommand, ListObjectVersionsCommand, S3Client } from "@aws-sdk/client-s3";
const client = new S3Client({
endpoint: "https://s3.eu-central-1.strg.com",
region: "eu-central-1",
});
const key = "photos/2024/cat.jpg";
const { Versions = [], DeleteMarkers = [] } = await client.send(
new ListObjectVersionsCommand({ Bucket: "my-bucket", Prefix: key }),
);
const objects = [...Versions, ...DeleteMarkers]
.filter((entry) => entry.Key === key)
.map(({ Key, VersionId }) => ({ Key, VersionId }));
const { Errors = [] } = await client.send(
new DeleteObjectsCommand({
Bucket: "my-bucket",
Delete: { Objects: objects, Quiet: true },
}),
);
for (const error of Errors) console.log("failed", error.Key, error.VersionId, error.Code, error.Message);