benbjohnson/litestream · error

s3: size ltx file %s: %w

Error message

s3: size ltx file %s: %w

What it means

uploadSizedLTX sizes the file by seeking to the end of the reader; when that Seek(0, io.SeekEnd) call fails, this error wraps the underlying cause. It means the io.ReadSeeker passed into the upload path could not be repositioned. This is an I/O failure on the local file, not a network/S3 problem.

Source

Thrown at s3/replica_client.go:779

// 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 {
			return c.uploadSizedLTX(ctx, key, rs, start, partSize)
		}
		// Reader does not support seeking (e.g. piped file descriptor);
		// nothing has been consumed, so treat it as an unsized stream.
	}
	return c.uploadStreamedLTX(ctx, key, r, partSize)
}

// uploadSizedLTX uploads from a seekable reader whose size is known.
func (c *ReplicaClient) uploadSizedLTX(ctx context.Context, key string, rs io.ReadSeeker, start, partSize int64) (int64, time.Time, *string, error) {
	end, err := rs.Seek(0, io.SeekEnd)
	if err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("s3: size ltx file %s: %w", key, err)
	}
	size := end - start
	if _, err := rs.Seek(start, io.SeekStart); err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("s3: rewind ltx file %s: %w", key, err)
	}

	// Extract timestamp from LTX header, then rewind so the upload sees the
	// full file.
	hdr, _, err := ltx.PeekHeader(rs)
	if err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("extract timestamp from LTX header: %w", err)
	}
	timestamp := time.UnixMilli(hdr.Timestamp).UTC()
	if _, err := rs.Seek(start, io.SeekStart); err != nil {
		return 0, time.Time{}, nil, fmt.Errorf("s3: rewind ltx file %s: %w", key, err)
	}

	input := c.putObjectInput(key, timestamp)

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Check the wrapped error (the %w cause) for the actual syscall failure (e.g. file not found, bad file descriptor)
  2. Ensure the file backing the reader still exists and the disk is healthy at upload time
  3. Pass a plain *os.File or other reliably seekable reader; avoid custom Reader wrappers that break Seek
  4. Retry the replication cycle — transient disk conditions often clear
Defensive patterns

Strategy: validation

Validate before calling

f, ok := r.(*os.File)
if !ok {
    return fmt.Errorf("upload requires a seekable *os.File, got %T", r)
}
if _, err := f.Seek(0, io.SeekEnd); err != nil {
    return fmt.Errorf("reader not seekable/healthy: %w", err)
}

Type guard

func isSeekableFile(r io.Reader) bool {
    _, ok := r.(*os.File)
    return ok
}

Try / catch

if err != nil && strings.Contains(err.Error(), "size ltx file") {
    // local I/O problem on the reader; check disk and file lifetime
}

Prevention

When it happens

Trigger: uploadLTX dispatches to uploadSizedLTX when the reader is seekable; the Seek to end fails because the underlying file descriptor is closed, the file was removed, or the reader reports an I/O error (disk issue, fd exhausted).

Common situations: Filesystem errors on the LTX staging file (disk full, detached network volume); a bug in calling code that passes a wrapped reader whose Seek method errors; race where the temp file is deleted before upload.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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