benbjohnson/litestream · error

cannot write to lock page

Error message

cannot write to lock page

What it means

The VFS Write path refuses any write whose target page is the reserved lock page (ltx.LockPgno), at the 1GB offset SQLite reserves. Writes are rejected with "cannot write to lock page" because LTX replication depends on that page remaining untouched.

Source

Thrown at vfs.go:1649

	return n, nil
}

func (f *VFSFile) WriteAt(b []byte, off int64) (n int, err error) {
	f.logger.Debug("write at", "off", off, "len", len(b))

	pageSize, err := f.pageSizeBytes()
	if err != nil {
		return 0, err
	}

	// Calculate page number and offset within page
	pgno := uint32(off/int64(pageSize)) + 1
	pageOffset := int(off % int64(pageSize))

	// Skip lock page
	if pgno == ltx.LockPgno(pageSize) {
		return 0, fmt.Errorf("cannot write to lock page")
	}

	f.mu.Lock()
	defer f.mu.Unlock()

	// If write support is not enabled, return read-only error
	if !f.writeEnabled {
		return 0, sqlite3vfs.ReadOnlyError
	}

	// Get page data - either from buffer file (if dirty) or from cache/remote
	page := make([]byte, pageSize)
	if bufferOff, ok := f.dirty[pgno]; ok {
		// Page is already dirty - read from buffer file
		if _, err := f.bufferFile.ReadAt(page, bufferOff); err != nil {
			return 0, fmt.Errorf("read dirty page from buffer: %w", err)
		}
	} else {

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure all writes go through SQLite/SQL (which skips the lock page), not raw file offsets.
  2. Skip the page at 0x40000000 in any byte-level copy/migration tooling.
  3. Verify database size and offset math: page number = off/pageSize + 1; never write pgno == ltx.LockPgno(pageSize).
  4. If a tool legitimately needs the region, restructure so that metadata lives elsewhere — the page must remain reserved.

Example fix

// before: raw write across the 1GB boundary
f.file.WriteAt(data, 0x40000000) // lock page

// after: skip the reserved lock page
if pgno == ltx.LockPgno(pageSize) {
    return 0, errors.New("lock page is reserved")
}
Defensive patterns

Strategy: validation

Validate before calling

pgno := uint32(off/int64(pageSize)) + 1
if pgno == ltx.LockPgno(uint32(pageSize)) {
    return errors.New("refusing write to reserved lock page at 0x40000000")
}

Try / catch

if _, err := file.WriteAt(data, off); err != nil {
    if strings.Contains(err.Error(), "cannot write to lock page") {
        return errors.New("raw writes crossing the 1GB lock page are unsupported; use SQL")
    }
    return err
}

Prevention

When it happens

Trigger: SQLite (or code writing raw byte offsets) issuing a Write at an offset whose computed page number equals ltx.LockPgno(pageSize) — i.e. an offset within [0x40000000, 0x40000000+pageSize). Direct raw-file writes or tools that don't respect the reserved page trigger it.

Common situations: Misbehaving tools migrating/copying databases byte-for-byte across the 1GB boundary; custom code computing offsets without skipping the lock page; databases growing past 1GB where writers ignore the reserved page.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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