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

  1. Remove the assignment; use the read form `PRAGMA litestream_txid;` to query the current TXID
  2. To travel to a point in time, use `PRAGMA litestream_time = '<RFC3339 or relative time or latest>'`
  3. If you need a specific TXID's corresponding time, resolve it from replica LTX files and pass that time to litestream_time
  4. 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

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


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