benbjohnson/litestream · error

unaligned wal offset %d for page size %d

Error message

unaligned wal offset %d for page size %d

What it means

The reader requires 'offset' to land exactly on a WAL frame boundary: after the 32-byte WAL header, frames are (pageSize+24) bytes each, so (offset-WALHeaderSize) must be a multiple of frameSize. This error is thrown when a caller passes an offset that does not correspond to the start of a frame for the page size stored in the WAL header.

Source

Thrown at wal_reader.go:64

	// 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)

	// Read previous page to load checksum. Context errors are returned as-is
	// so callers don't mistake a cancellation for a frame mismatch.
	r.frameN--
	if _, _, err := r.readFrame(ctx, make([]byte, r.pageSize), false); err != nil {
		if ctx.Err() != nil {
			return nil, context.Cause(ctx)
		}
		return nil, &PrevFrameMismatchError{Err: err}
	}

	return r, nil
}

// PageSize returns the page size from the header. Must call ReadHeader() first.
func (r *WALReader) PageSize() uint32 { return r.pageSize }

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Persist and reuse the offset together with the page size, and validate the pair before reopening
  2. Only pass offsets previously returned by the reader (frame starts), never hand-computed values
  3. Recompute alignment: offset must equal WALHeaderSize + n*(pageSize+24); if your stored offset fails, reset to 0 and re-read the WAL from the start
  4. If the page size changed, restart replication from the beginning of the current WAL

Example fix

// before
frameSize := pageSize + WALFrameHeaderSize
r, err := NewWALReaderWithOffset(ctx, f, savedOffset, logger)
// after: check alignment first
frameSize := int64(pageSize + WALFrameHeaderSize)
if (savedOffset-WALHeaderSize)%frameSize != 0 {
    savedOffset = WALHeaderSize // fall back to start of WAL
}
r, err := NewWALReaderWithOffset(ctx, f, savedOffset, logger)
Defensive patterns

Strategy: validation

Validate before calling

func alignedWALOffset(offset, pageSize int64) bool {
    frameSize := pageSize + 24
    return offset >= 32 && (offset-32)%int64(frameSize) == 0
}

Try / catch

r, err := NewWALReaderWithOffset(ctx, f, off, logger)
var alignErr interface{ Error() string }
if err != nil && strings.Contains(err.Error(), "unaligned wal offset") {
    off = 32 // restart from beginning of WAL
    r, err = NewWALReaderWithOffset(ctx, f, off, logger)
}

Prevention

When it happens

Trigger: Passing an arbitrary byte offset to NewWALReaderWithOffset — e.g. an offset saved against a different page size, an offset captured mid-frame, or an offset from a different WAL file.

Common situations: Resuming replication from a persisted offset after the database page size changed (VACUUM/PRAGMA page_size); restoring an offset from config that was recorded for another database; off-by-one when computing frame offsets manually.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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