{"record":{"id":"72192b5cb1c19fcf","repo":"benbjohnson/litestream","slug":"cannot-write-to-lock-page","errorCode":null,"errorMessage":"cannot write to lock page","messagePattern":"cannot write to lock page","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"vfs.go","lineNumber":1649,"sourceCode":"\n\treturn n, nil\n}\n\nfunc (f *VFSFile) WriteAt(b []byte, off int64) (n int, err error) {\n\tf.logger.Debug(\"write at\", \"off\", off, \"len\", len(b))\n\n\tpageSize, err := f.pageSizeBytes()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// Calculate page number and offset within page\n\tpgno := uint32(off/int64(pageSize)) + 1\n\tpageOffset := int(off % int64(pageSize))\n\n\t// Skip lock page\n\tif pgno == ltx.LockPgno(pageSize) {\n\t\treturn 0, fmt.Errorf(\"cannot write to lock page\")\n\t}\n\n\tf.mu.Lock()\n\tdefer f.mu.Unlock()\n\n\t// If write support is not enabled, return read-only error\n\tif !f.writeEnabled {\n\t\treturn 0, sqlite3vfs.ReadOnlyError\n\t}\n\n\t// Get page data - either from buffer file (if dirty) or from cache/remote\n\tpage := make([]byte, pageSize)\n\tif bufferOff, ok := f.dirty[pgno]; ok {\n\t\t// Page is already dirty - read from buffer file\n\t\tif _, err := f.bufferFile.ReadAt(page, bufferOff); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"read dirty page from buffer: %w\", err)\n\t\t}\n\t} else {","sourceCodeStart":1631,"sourceCodeEnd":1667,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/vfs.go#L1631-L1667","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: raw write across the 1GB boundary\nf.file.WriteAt(data, 0x40000000) // lock page\n\n// after: skip the reserved lock page\nif pgno == ltx.LockPgno(pageSize) {\n    return 0, errors.New(\"lock page is reserved\")\n}","handlingStrategy":"validation","validationCode":"pgno := uint32(off/int64(pageSize)) + 1\nif pgno == ltx.LockPgno(uint32(pageSize)) {\n    return errors.New(\"refusing write to reserved lock page at 0x40000000\")\n}","typeGuard":null,"tryCatchPattern":"if _, err := file.WriteAt(data, off); err != nil {\n    if strings.Contains(err.Error(), \"cannot write to lock page\") {\n        return errors.New(\"raw writes crossing the 1GB lock page are unsupported; use SQL\")\n    }\n    return err\n}","preventionTips":["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."],"tags":["write-path","lock-page","sqlite-internals","litestream"],"backgroundTag":"unsupported-operation","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}