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
- Ensure all writes go through SQLite/SQL (which skips the lock page), not raw file offsets.
- Skip the page at 0x40000000 in any byte-level copy/migration tooling.
- Verify database size and offset math: page number = off/pageSize + 1; never write pgno == ltx.LockPgno(pageSize).
- 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
- Never write the database file byte-for-byte with raw offsets; use SQLite APIs.
- In migration tools, split data at 0x40000000 and skip the reserved page (see docs/SQLITE_INTERNALS.md).
- Add an assertion in tooling that pgno != ltx.LockPgno(pageSize) before any WriteAt.
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
- too many arguments
- failed to format response: %w
- database config #%d: 'watch' can only be enabled with a dire
- database config #%d: 'meta-dir' can only be used with a dire
- database config #%d: cannot specify both 'meta-path' and 'me
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/72192b5cb1c19fcf.
Report an issue: GitHub.