benbjohnson/litestream · error
litestream_txid is read-only
Error message
litestream_txid is read-only
What it means
The litestream_txid PRAGMA is read-only: FileControl() in vfs.go returns this error when pragmaValue is non-nil, i.e. the caller attempted `PRAGMA litestream_txid = <value>` to assign to it. Reading (pragmaValue == nil) is valid and returns the current replication position's TXID string.
Source
Thrown at vfs.go:2439
return result.Time.UTC(), nil
}
// FileControl handles file control operations, specifically PRAGMA commands for time travel.
func (f *VFSFile) FileControl(op int, pragmaName string, pragmaValue *string) (*string, error) {
const SQLITE_FCNTL_PRAGMA = 14
if op != SQLITE_FCNTL_PRAGMA {
return nil, fmt.Errorf("unsupported file control op: %d", op)
}
name := strings.ToLower(pragmaName)
f.logger.Debug("file control", "pragma", name, "value", pragmaValue)
switch name {
case "litestream_txid":
if pragmaValue != nil {
return nil, fmt.Errorf("litestream_txid is read-only")
}
txid := f.Pos().TXID
result := txid.String()
return &result, nil
case "litestream_lag":
if pragmaValue != nil {
return nil, fmt.Errorf("litestream_lag is read-only")
}
lastPoll := f.LastPollSuccess()
if lastPoll.IsZero() {
result := "-1" // Never polled successfully
return &result, nil
}
lag := int64(time.Since(lastPoll).Seconds())
result := strconv.FormatInt(lag, 10)
return &result, nil
View on GitHub (pinned to 4ed7a308f6)
Solutions
- Remove the assignment; use the read form `PRAGMA litestream_txid;` to query the current TXID
- To travel to a point in time, use `PRAGMA litestream_time = '<RFC3339 or relative time or latest>'`
- If you need a specific TXID's corresponding time, resolve it from replica LTX files and pass that time to litestream_time
- Catch the error and surface a clear message that txid is read-only
Example fix
// before PRAGMA litestream_txid = '00000000-0000-0000-0000-000000000000'; // after PRAGMA litestream_txid; -- read the current TXID PRAGMA litestream_time = '5 minutes ago'; -- travel in time
Defensive patterns
Strategy: validation
Validate before calling
// Reject assignment form before executing
if strings.Contains(strings.ToLower(stmt), "litestream_txid=") ||
strings.Contains(strings.ToLower(stmt), "litestream_txid =") {
return errors.New("litestream_txid is read-only; use the read form PRAGMA litestream_txid;")
} Try / catch
_, err := db.Exec("PRAGMA litestream_txid = ?", v)
if err != nil && strings.Contains(err.Error(), "litestream_txid is read-only") {
// switch to read-only usage
row := db.QueryRow("PRAGMA litestream_txid")
var txid string
_ = row.Scan(&txid)
} Prevention
- Only ever query litestream_txid; never use the assignment form
- Use PRAGMA litestream_time for time travel instead of trying to set a TXID
- Sanitize/ban write-forms of litestream_* pragmas in ORM/DB helper layers
When it happens
Trigger: Executing `PRAGMA litestream_txid = 'xxxx'` (assignment form) on a connection backed by the litestream VFS. In SQLite, the assignment form passes a non-nil pragmaValue into xFileControl, triggering the guard.
Common situations: Developers try to 'set' the transaction id, e.g. to force time travel to a specific TXID or restore state, not realizing TXID is an observed property of the replica position, not a writable setting. Time travel is done via PRAGMA litestream_time instead.
Related errors
- litestream_lag is read-only
- litestream_hydration_progress is read-only
- could not parse time: %s
- litestream_hydration_file is read-only
- set page size: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/9c5d054a23df8538.
Report an issue: GitHub.