benbjohnson/litestream · error

WALReader.ReadFrame(): buffer size (%d) must match page size

Error message

WALReader.ReadFrame(): buffer size (%d) must match page size (%d)

What it means

ReadFrame/readFrame fill the caller-supplied buffer with one page per frame. The buffer must be exactly pageSize bytes (as declared in the WAL header); this error is thrown when len(data) differs from the WAL's page size, so the frame read would overflow or underfill the buffer.

Source

Thrown at wal_reader.go:149

	return nil
}

// ReadFrame reads the next frame from the WAL and returns the page number.
// Returns io.EOF at the end of the valid WAL.
func (r *WALReader) ReadFrame(ctx context.Context, data []byte) (pgno, commit uint32, err error) {
	return r.readFrame(ctx, data, true)
}

func (r *WALReader) readFrame(ctx context.Context, data []byte, verifyChecksum bool) (pgno, commit uint32, err error) {
	select {
	case <-ctx.Done():
		return 0, 0, context.Cause(ctx)
	default:
	}

	if len(data) != int(r.pageSize) {
		return 0, 0, fmt.Errorf("WALReader.ReadFrame(): buffer size (%d) must match page size (%d)", len(data), r.pageSize)
	}

	frameSize := r.pageSize + WALFrameHeaderSize
	offset := WALHeaderSize + (int64(r.frameN) * int64(frameSize))

	// Read WAL frame header.
	hdr := make([]byte, WALFrameHeaderSize)
	if n, err := r.r.ReadAt(hdr, offset); n != len(hdr) {
		return 0, 0, io.EOF
	} else if err != nil {
		return 0, 0, err
	}

	// Read WAL page data.
	if n, err := r.r.ReadAt(data, offset+WALFrameHeaderSize); n != len(data) {
		return 0, 0, io.EOF
	} else if err != nil {
		return 0, 0, err

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Query the actual page size (PRAGMA page_size) and allocate the read buffer from it, not a constant
  2. Obtain the page size from the WALReader (r.pageSize / pageSize field) after NewWALReader* returns, then size the buffer
  3. If the page size changed mid-stream, re-open the WAL reader so the new header/page size is picked up
  4. Never reuse a buffer sized for a previous page size across readers

Example fix

// before
data := make([]byte, 4096)
n0, n1, err := reader.ReadFrame(ctx, data)
// after
data := make([]byte, reader.PageSize())
n0, n1, err := reader.ReadFrame(ctx, data)
Defensive patterns

Strategy: validation

Validate before calling

// derive the buffer from the reader, never a constant
buf := make([]byte, r.PageSize())
if len(buf) != int(r.PageSize()) { panic("page size mismatch") }

Try / catch

_, _, err := reader.ReadFrame(ctx, data)
if err != nil && strings.Contains(err.Error(), "must match page size") {
    data = make([]byte, reader.PageSize()) // re-size and retry
    _, _, err = reader.ReadFrame(ctx, data)
}

Prevention

When it happens

Trigger: Calling ReadFrame with a buffer sized from a stale or assumed page size while the WAL header declares a different one; using the default 4096-byte buffer against a database with page_size=8192 etc.

Common situations: Database page size changed via 'PRAGMA page_size' or VACUUM between replication runs; hardcoding 4096 in code that reads frames; mixing readers across databases with different page sizes.

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/6e7d7ad294b966cf. Report an issue: GitHub.