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

  1. Check disk space and inode/quota on the volume holding the hydration file; free space or move the hydration path to a larger volume.
  2. Verify no concurrent code path closes or truncates h.file (reset/disable) during ApplyLTX; serialize with h.mu or lifecycle checks.
  3. If the file handle is stale after reset, re-create the hydration file (createHydrationFile) and re-run the full Restore.
  4. 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

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


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