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 <x.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
- Retry the upload; a transient gateway glitch usually returns a proper ETag on a second attempt
- Verify the endpoint is fully S3-compatible (test a plain aws s3 cp and inspect returned ETag)
- Upgrade the endpoint software (e.g. MinIO) to a version that returns ETag on PutObject
- 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
- Test S3-compatible endpoints return ETag headers before production use
- Prefer well-known S3 implementations (AWS S3, MinIO current releases)
- Monitor for repeated ETag errors per endpoint as a compatibility signal
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
- oss: upload failed: no ETag returned
- s3: upload to %s: part size must be at least %d bytes
- s3: put object %s: %w
- upload LTX: %w
- lease not held
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/c63b9288b6bdd93c.
Report an issue: GitHub.