S3 external binary storage: DeleteObjects fails with missing Content-MD5

Hi n8n team and community,

We are using n8n with external S3 binary data storage backed by Dell ECS (an S3-compatible object storage solution).

We have encountered an issue with the deletion of binary objects, specifically when n8n uses the S3 DeleteObjects API.

What we are observing

When n8n deletes binary data (for example, during execution data pruning), the AWS SDK sends a multi-object delete request with CRC32 checksum headers.

However, our Dell ECS S3-compatible endpoint expects a Content-MD5 header for DeleteObjects requests and rejects the request when it is missing.

The consequence is that the corresponding binary data records have already been removed from the n8n database, but the actual objects remain in the S3 bucket.

Over time, this can result in orphaned binary objects and unnecessary storage consumption.

Questions for the n8n team

  1. Is there a reason Content-MD5 is not explicitly supplied for DeleteObjects, given that it is already calculated for object uploads?

  2. Would it be possible to support Content-MD5 for multi-object deletes, or provide a configurable checksum strategy for S3-compatible storage providers?

Has anyone else encountered this issue with S3-compatible storage providers?

Appreciate any feedback or guidance from the n8n team.

Thanks!

Hey @Vineet18 Welcome to the n8n community!!

n8n’s current source explicitly supplies Content-MD5 for uploads, but its bulk-delete call leaves checksums to the AWS SDK. AWS documents this exact CRC32 compatibility issue and provides MD5 fallback middleware. Applying that fallback would require a change to n8n’s storage client. It hashes the serialized DeleteObjects XML before signing, so the uploaded file’s checksum can’t be reused.

Which n8n version are you running, and can you share the pruning error and stack trace? Current n8n docs delegate S3 binary retention to bucket lifecycle rules. That addresses binary buildup separately from the checksum fix. Scope expiration to binary objects and align its retention with your execution history.

Thanks, that matches what I found. n8n versions is 2.45.6.

Error from the pruning task:

{“level”:“error”,“message”:“Failed to hard-delete executions”,“metadata”:{“executionIds”:[“4743”,“4742”,“4741”,“4740”,“4739”,“4738”,“4737”,“4736”,“4735”,“4734”,“4733”,“4732”,“4731”,“4730”,“4729”,“4728”,“4727”,“4726”,“4725”,“4724”,“4723”,“4722”,“4721”,“4720”,“4719”,“4718”,“4717”,“4716”,“4714”,“4712”,“4711”,“4710”,“4709”,“4708”,“4707”,“4706”,“4705”,“4704”,“4700”,“4699”,“4698”,“4695”,“4694”,“4693”,“4692”,“4691”,“4690”,“4689”,“4688”,“4687”,“4674”,“4673”,“4672”,“4671”,“4670”,“4669”],“file”:“executions-pruning.service.js”,“function”:“hardDeleteOneByOne”,“scopes”:[“pruning”],“timestamp”:“2026-11-03T21:56:52.664Z”}}{“level”:“error”,“message”:“A system task run failed”,“metadata”:{“error”:{“cause”:{“message”:“Missing required header for this request: Content-MD5”,“name”:“InvalidRequest”,“stack”:“InvalidRequest: Missing required header for this request: Content-MD5\n at de_InvalidRequestRes (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+client-s3@3.808.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js:5005:21)\n at de_CommandError (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+client-s3@3.808.0/node_modules/@aws-sdk/client-s3/dist-cjs/index.js:4940:19)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@smithy+middleware-serde@4.2.15/node_modules/@smithy/middleware-serde/dist-cjs/index.js:9:24\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+middleware-sdk-s3@3.808.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-cjs/index.js:484:18\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@smithy+middleware-retry@4.4.12/node_modules/@smithy/middleware-retry/dist-cjs/index.js:254:46\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+middleware-flexible-checksums@3.808.0/node_modules/@aws-sdk/middleware-flexible-checksums/dist-cjs/index.js:318:18\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+middleware-sdk-s3@3.808.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-cjs/index.js:110:22\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+middleware-sdk-s3@3.808.0/node_modules/@aws-sdk/middleware-sdk-s3/dist-cjs/index.js:137:14\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/@aws-sdk+middleware-logger@3.804.0/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:33:22”},“message”:“Request to S3 failed: Missing required header for this request: Content-MD5”,“name”:“Error”,“stack”:“Error: Request to S3 failed: Missing required header for this request: Content-MD5\n at ObjectStoreService.handleS3Error (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/+blob-storage@file++++home+runner+_work+n8n+n8n+packages++blob-storage/node_modules//blob-storage/src/object-store/object-store.service.ee.ts:397:9)\n at ObjectStoreService.deleteByKeys (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/+blob-storage@file++++home+runner+_work+n8n+n8n+packages++blob-storage/node_modules//blob-storage/src/object-store/object-store.service.ee.ts:321:9)\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at S3ByteStore.delete (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/+blob-storage@file++++home+runner+_work+n8n+n8n+packages++blob-storage/node_modules//blob-storage/src/s3-byte-store.ee.ts:53:3)\n at /usr/local/lib/node_modules/n8n/node_modules/.pnpm/+blob-storage@file++++home+runner+_work+n8n+n8n+packages++blob-storage/node_modules//blob-storage/src/json-store.ts:86:5\n at async Promise.all (index 0)\n at ExecutionDataJsonStore.delete (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/+blob-storage@file++++home+runner+_work+n8n+n8n+packages++blob-storage/node_modules//blob-storage/src/json-store.ts:79:3)\n at async Promise.all (index 1)\n at ExecutionPersistence.hardDelete (/usr/local/lib/node_modules/n8n/src/executions/execution-persistence.ts:654:3)\n at ExecutionsPruningService.hardDeleteBatch (/usr/local/lib/node_modules/n8n/src/services/pruning/executions-pruning.service.ts:73:4)”},“file”:“system-task-runner.js”,“function”:“reportFailure”,“name”:“execution-pruning-hard-delete”,“scopes”:[“system-tasks”],“timestamp”:“2026-11-03T21:56:52.665Z”}}

Hey @Vineet18
That trace confirms execution JSON pruning is failing. Apply this patch to ObjectStoreService.deleteByKeys() in 2.45.6, then rebuild and redeploy n8n. It adds Content-MD5 over the serialized DeleteObjects request before signing.

The execution rows are retained for retry when cleanup fails, so pruning can retry them after the fix. My earlier binary lifecycle suggestion doesn’t resolve this execution JSON path.

Please refer the reply below.

--- a/packages/@n8n/blob-storage/src/object-store/object-store.service.ee.ts
+++ b/packages/@n8n/blob-storage/src/object-store/object-store.service.ee.ts
@@ -1,6 +1,7 @@
 import type {
 	PutObjectCommandInput,
 	DeleteObjectsCommandInput,
+	DeleteObjectsCommandOutput,
 	ListObjectsV2CommandInput,
 	S3ClientConfig,
 } from '@aws-sdk/client-s3';
@@ -31,6 +32,12 @@
 /** How many per-key delete failures to name in the error before truncating, to keep the message bounded. */
 const MAX_REPORTED_DELETE_ERRORS = 5;
 
+type DeleteObjectsHandler = (args: {
+	input: DeleteObjectsCommandInput;
+	request: unknown;
+}) => Promise<{ output: DeleteObjectsCommandOutput; response: unknown }>;
+
+
 @Service()
 export class ObjectStoreService {
 	private s3Client: S3Client;
@@ -300,7 +307,31 @@
 				});
 
 				// `DeleteObjects` reports per-key failures in the response rather than failing the request
-				const { Errors: errors } = await this.s3Client.send(new DeleteObjectsCommand(params));
+				const command = new DeleteObjectsCommand(params);
+				command.middlewareStack.addRelativeTo(
+					(next: DeleteObjectsHandler) => async (args: Parameters<DeleteObjectsHandler>[0]) => {
+						const request = args.request as {
+							headers: Record<string, string>;
+							body: string | Uint8Array;
+						};
+						for (const header of Object.keys(request.headers)) {
+							const name = header.toLowerCase();
+							if (name.startsWith('x-amz-checksum-') || name.startsWith('x-amz-sdk-checksum-')) {
+								delete request.headers[header];
+							}
+						}
+						request.headers['Content-MD5'] = createHash('md5')
+							.update(request.body)
+							.digest('base64');
+						return await next(args);
+					},
+					{
+						relation: 'after',
+						toMiddleware: 'flexibleChecksumsMiddleware',
+						name: 'addMD5ChecksumForDeleteObjects',
+					},
+				);
+				const { Errors: errors } = await this.s3Client.send(command);
 
 				if (errors && errors.length > 0) {
 					this.logger.error('Failed to delete objects from S3', {

Newer AWS SDK versions add CRC32 checksums by default, which a lot of S3 compatible stores reject. Try setting AWS_REQUEST_CHECKSUM_CALCULATION=when_required on the n8n container. For DeleteObjects the SDK should then fall back to Content-MD5. Worth testing on a spare bucket first.