benbjohnson/litestream · error
open ltx file
Error message
open ltx file: %w
What it means
FetchLTXHeader opens a specific LTX file on the replica (client.OpenLTXFile, restricted to the header bytes) and this error wraps the open failure. Per the RAISED-IN note, it surfaces through Init and related flows (restoreIfNotExists, LTXFiles, WriteLTXFile, OpenLTXFile, DeleteLTXFiles, DeleteAll), meaning an LTX file recorded in the listing could not actually be opened for reading.
Solutions
- Re-list the replica (litestream ltx / ValidateLevel) to get current file info — the referenced file may have been removed by retention or compaction
- Verify storage credentials allow object reads (e.g. s3:GetObject) and the bucket/endpoint config is correct
- If local state is inconsistent, run 'litestream reset' for the database or perform a fresh 'litestream restore'
- Check for concurrent processes deleting from the same replica (single replica per database rule)
Example fix
// before: opening a FileInfo captured long ago rc, err := client.OpenLTXFile(ctx, staleInfo.Level, staleInfo.MinTXID, staleInfo.MaxTXID, 0, ltx.HeaderSize) // after: re-fetch fresh file info right before opening info, err := findFile(ctx, client, level, txid) // fresh listing rc, err := client.OpenLTXFile(ctx, info.Level, info.MinTXID, info.MaxTXID, 0, ltx.HeaderSize)
Defensive patterns
Strategy: retry
Validate before calling
// Re-list to confirm the file still exists before opening
itr, err := client.LTXFiles(ctx, info.Level, info.MinTXID, false)
if err == nil {
var found bool
for itr.Next() { if itr.Item().MaxTXID == info.MaxTXID { found = true; break } }
if !found { return fmt.Errorf("LTX file %s no longer present", info.MaxTXID) }
} Type guard
func fileStillListed(ctx context.Context, c ReplicaClient, info *ltx.FileInfo) bool {
itr, err := c.LTXFiles(ctx, info.Level, info.MinTXID, false)
if err != nil { return false }
defer itr.Close()
for itr.Next() {
it := itr.Item()
if it.MinTXID == info.MinTXID && it.MaxTXID == info.MaxTXID { return true }
}
return false
} Try / catch
hdr, err := FetchLTXHeader(ctx, client, info)
if err != nil {
if errors.Is(err, os.ErrNotExist) || isNotFoundErr(err) {
// re-list and pick a fresher restore candidate
}
return err
} Prevention
- Do not run retention cleanup concurrently with restore/validation
- Always work from a freshly fetched file listing, not cached FileInfo
- Verify storage read permissions (e.g. s3:GetObject) in IAM policy
- Use 'litestream reset' when local state references missing objects
When it happens
Trigger: OpenLTXFile called with (level, MinTXID, MaxTXID) that does not resolve to a stored object: the file was deleted by retention/compaction between listing and open, the object name is wrong, storage credentials lack read access, or the object is missing in the bucket/container.
Common situations: A retention job or manual cleanup deleted LTX files while a restore/validation was in progress; replica URL points to a bucket where compaction removed the referenced generation; misconfigured endpoint causing 404-style open failures; IAM policy missing s3:GetObject.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/757b9d74997f7b22.
Report an issue: GitHub.
Appendix: source
Thrown at replica_client.go:104
// DefaultEstimatedPageIndexSize is size that is first fetched when fetching the page index.
// If the fetch was smaller than the actual page index, another call is made to fetch the rest.
const DefaultEstimatedPageIndexSize = 32 * 1024 // 32KB
func FetchPageIndex(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (map[uint32]ltx.PageIndexElem, error) {
rc, err := fetchPageIndexData(ctx, client, info)
if err != nil {
return nil, err
}
defer rc.Close()
return ltx.DecodePageIndex(bufio.NewReader(rc), info.Level, info.MinTXID, info.MaxTXID)
}
// FetchLTXHeader reads & returns the LTX header for the given file info.
func FetchLTXHeader(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (ltx.Header, error) {
rc, err := client.OpenLTXFile(ctx, info.Level, info.MinTXID, info.MaxTXID, 0, ltx.HeaderSize)
if err != nil {
return ltx.Header{}, fmt.Errorf("open ltx file: %w", err)
}
defer rc.Close()
hdr, _, err := ltx.PeekHeader(rc)
if err != nil {
return ltx.Header{}, fmt.Errorf("peek header: %w", err)
}
return hdr, nil
}
// fetchPageIndexData fetches a chunk of the end of the file to get the page index.
// If the fetch was smaller than the actual page index, another call is made to fetch the rest.
func fetchPageIndexData(ctx context.Context, client ReplicaClient, info *ltx.FileInfo) (io.ReadCloser, error) {
// Fetch the end of the file to get the page index.
offset := info.Size - DefaultEstimatedPageIndexSize
if offset < 0 {
offset = 0
}
View on GitHub (pinned to 4ed7a308f6)