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
- Ensure the offset passed reflects the start of a frame after the 32-byte WAL header (offset > 32)
- Use NewWALReader (header-based) instead when starting from the beginning of the WAL
- 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
- Persist the sync offset only after successfully reading frames (never 0)
- Use NewWALReader for initial reads and WithOffset only for resumption
- Guard public constructor calls with an offset > WALHeaderSize check
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
- snapshot interval must be greater than 0
- snapshot retention must be greater than 0
- compaction interval must be greater than 0
- sync interval must be greater than 0
- l0 retention must not be negative
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/5bf95b1b434f379d.
Report an issue: GitHub.