benbjohnson/litestream · error

offset (%d) must be greater than the wal header size (%d)

Error message

offset (%d) must be greater than the wal header size (%d)

What it means

NewWALReaderWithOffset resumes reading the WAL at a given frame offset, but it must read the previous page to seed the checksum, so the offset must be strictly greater than the WAL header size (32 bytes). Offsets at or below the header size are rejected because there is no preceding page to compute the running checksum from.

Source

Thrown at wal_reader.go:48

	logger *slog.Logger
}

// NewWALReader returns a new instance of WALReader.
func NewWALReader(rd io.ReaderAt, logger *slog.Logger) (*WALReader, error) {
	r := &WALReader{r: rd, logger: logger}
	if err := r.readHeader(); err != nil {
		return nil, err
	}
	return r, nil
}

// NewWALReaderWithOffset returns a new instance of WALReader at a given offset.
// Salt must match or else no frames will be returned. Checksum calculated from
// from previous page.
func NewWALReaderWithOffset(ctx context.Context, rd io.ReaderAt, offset int64, salt1, salt2 uint32, logger *slog.Logger) (*WALReader, error) {
	// Ensure we are not starting on the first page since we need to read the previous.
	if offset <= WALHeaderSize {
		return nil, fmt.Errorf("offset (%d) must be greater than the wal header size (%d)", offset, WALHeaderSize)
	}

	r := &WALReader{r: rd, logger: logger}

	// Read header to determine page size & byte order.
	if err := r.readHeader(); err != nil {
		return nil, fmt.Errorf("read header: %w", err)
	}

	// Load in salt in case the beginning of the file has been overwritten.
	r.salt1, r.salt2 = salt1, salt2

	// Ensure offset is positioned on a frame start.
	frameSize := int64(r.pageSize + WALFrameHeaderSize)
	if (offset-WALHeaderSize)%frameSize != 0 {
		return nil, fmt.Errorf("unaligned wal offset %d for page size %d", offset, r.pageSize)
	}
	r.frameN = int((offset - WALHeaderSize) / frameSize)

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure the offset passed reflects the start of a frame after the 32-byte WAL header (offset > 32)
  2. Use NewWALReader (header-based) instead when starting from the beginning of the WAL
  3. Reset the DB's stored sync offset (e.g. via litestream reset) if persisted state is corrupt and re-sync

Example fix

// before
r, err := NewWALReaderWithOffset(ctx, rd, 0, salt1, salt2, logger)
// after
r, err := NewWALReader(ctx, rd, logger) // start from header
// or: offset must point at a frame beyond the 32-byte header
Defensive patterns

Strategy: validation

Validate before calling

if offset <= WALHeaderSize {
    // fall back to NewWALReader which starts from the header
    return NewWALReader(ctx, rd, logger)
}

Type guard

func validSyncOffset(offset int64) bool { return offset > WALHeaderSize }

Prevention

When it happens

Trigger: Calling NewWALReaderWithOffset with offset <= WALHeaderSize — e.g. offset 0 or 32 — from the DB's sync path, typically when the saved sync offset was never advanced past the WAL header or was reset/corrupted.

Common situations: A freshly created WAL whose stored offset was persisted as 0; corrupt or truncated checkpoint metadata; calling the public constructor directly with a hand-computed offset that includes the header.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


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