benbjohnson/litestream · error

s3: upload failed: no ETag returned

Error message

s3: upload failed: no ETag returned

What it means

After a successful PutObject/upload the S3 response carried no ETag, which litestream treats as proof the upload did not actually persist. The SDK normally guarantees an ETag on success, so a nil ETag indicates an anomalous or non-standard S3-compatible endpoint. The upload is reported as failed so replication is retried rather than recorded as done.

Source

Thrown at s3/replica_client.go:748

	// The uploader rejects part sizes below the SDK minimum on every upload.
	// Fail identically here so the single-put path cannot mask the
	// misconfiguration for small objects.
	if partSize < manager.MinUploadPartSize {
		return nil, fmt.Errorf("s3: upload to %s: part size must be at least %d bytes", key, manager.MinUploadPartSize)
	}

	size, timestamp, etag, err := c.uploadLTX(ctx, key, r, partSize)
	if err != nil {
		return nil, err
	}

	internal.OperationTotalCounterVec.WithLabelValues(ReplicaClientType, "PUT").Inc()
	internal.OperationBytesCounterVec.WithLabelValues(ReplicaClientType, "PUT").Add(float64(size))

	// ETag indicates successful upload
	if etag == nil {
		return nil, fmt.Errorf("s3: upload failed: no ETag returned")
	}

	return &ltx.FileInfo{
		Level:     level,
		MinTXID:   minTXID,
		MaxTXID:   maxTXID,
		Size:      size,
		CreatedAt: timestamp,
	}, nil
}

// uploadLTX uploads LTX data from r to key, choosing between a single
// PutObject and the multipart uploader based on the object size.
func (c *ReplicaClient) uploadLTX(ctx context.Context, key string, r io.Reader, partSize int64) (int64, time.Time, *string, error) {
	// The L0 replication path passes the local LTX file, so the size is
	// known up front and the body stays seekable for SDK retries.
	if rs, ok := r.(io.ReadSeeker); ok {
		if start, err := rs.Seek(0, io.SeekCurrent); err == nil {

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Retry the upload; a transient gateway glitch usually returns a proper ETag on a second attempt
  2. Verify the endpoint is fully S3-compatible (test a plain aws s3 cp and inspect returned ETag)
  3. Upgrade the endpoint software (e.g. MinIO) to a version that returns ETag on PutObject
  4. If the provider fundamentally never returns ETags, it is unsupported by litestream's S3 replica client; use a different replica backend

Example fix

null
Defensive patterns

Strategy: retry

Validate before calling

null

Try / catch

info, err := client.WriteLTXFile(ctx, key, r, level, minTXID, maxTXID, ts, partSize)
if err != nil && strings.Contains(err.Error(), "no ETag returned") {
    // retry once after backoff; if persistent, endpoint is not S3-compatible
}

Prevention

When it happens

Trigger: Calling WriteLTXFile against an S3-compatible storage provider (MinIO, Ceph, proprietary gateways, some NAS S3 gateways) whose PutObject response omits the ETag header.

Common situations: Using a third-party S3-compatible endpoint that does not fully implement the S3 API; a proxy or CDN stripping response headers; unusual multipart upload paths where the SDK fails to aggregate the ETag.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/c63b9288b6bdd93c. Report an issue: GitHub.