benbjohnson/litestream · error
write page %d: %w
Error message
write page %d: %w
What it means
After decoding a page, ApplyLTX writes it to the local hydration file at offset (Pgno-1)*pageSize via WriteAt; failure is wrapped as "write page %d" with the page number. This is a local disk I/O failure, not a replication problem — the hydrated database file cannot be updated.
Source
Thrown at vfs.go:870
return fmt.Errorf("decode header: %w", err)
}
h.mu.Lock()
defer h.mu.Unlock()
// Apply each page to the hydration file
for {
var phdr ltx.PageHeader
data := make([]byte, h.pageSize)
if err := dec.DecodePage(&phdr, data); err == io.EOF {
break
} else if err != nil {
return fmt.Errorf("decode page: %w", err)
}
off := int64(phdr.Pgno-1) * int64(h.pageSize)
if _, err := h.file.WriteAt(data, off); err != nil {
return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
}
}
return nil
}
// ReadAt reads data from the hydrated local file.
func (h *Hydrator) ReadAt(p []byte, off int64) (int, error) {
h.mu.Lock()
n, err := h.file.ReadAt(p, off)
h.mu.Unlock()
if err != nil && err != io.EOF {
return n, fmt.Errorf("read hydrated file: %w", err)
}
// Update the first page to pretend like we are in journal mode
if off == 0 && len(p) >= 28 {View on GitHub (pinned to 4ed7a308f6)
Solutions
- Check disk space and inode/quota on the volume holding the hydration file; free space or move the hydration path to a larger volume.
- Verify no concurrent code path closes or truncates h.file (reset/disable) during ApplyLTX; serialize with h.mu or lifecycle checks.
- If the file handle is stale after reset, re-create the hydration file (createHydrationFile) and re-run the full Restore.
- Inspect dmesg/journal for hardware I/O errors (EIO) and replace failing storage if present.
Example fix
// before
if _, err := h.file.WriteAt(data, off); err != nil {
return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
}
// after
if _, err := h.file.WriteAt(data, off); err != nil {
if errors.Is(err, syscall.ENOSPC) {
return fmt.Errorf("write page %d: disk full at %s: %w", phdr.Pgno, h.path, err)
}
return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
if st, err := h.file.Stat(); err != nil {
return fmt.Errorf("hydration file unavailable: %w", err)
}
if free, err := diskFree(filepath.Dir(h.path)); err == nil && free < minFreeBytes {
return fmt.Errorf("insufficient disk space for hydration")
} Try / catch
if _, err := h.file.WriteAt(data, off); err != nil {
var pe *fs.PathError
if errors.As(err, &pe) && errors.Is(pe.Err, syscall.ENOSPC) {
// free space or fail over to another volume, then re-run restore
}
return fmt.Errorf("write page %d: %w", phdr.Pgno, err)
} Prevention
- Monitor disk usage/inodes on the hydration volume and alert before full
- Do not run `litestream reset` concurrently with ApplyLTX
- Place the hydration file on durable local storage, not ephemeral tmpfs
When it happens
Trigger: Calling ApplyLTX when the hydration file's volume is full, the file descriptor was closed by another goroutine, the file was truncated/deleted underneath the hydrator, or an OS-level I/O error (EIO, EDQUOT, ENOSPC) occurs.
Common situations: Disk quota exceeded on the node; tmpfs/small ephemeral storage for the hydration path; container volume detached mid-run; concurrent `litestream reset` closing the file while ApplyLTX writes.
Understand the failure class
Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.
Related errors
- write updated page %d: %w
- write page %d to hydrated file: %w
- failed to read response: %w
- failed to scan directory %s: %w
- failed to read response: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/187f21b46867d182.
Report an issue: GitHub.