benbjohnson/litestream · error

open db file descriptor: %w

Error message

open db file descriptor: %w

What it means

After enabling PERSIST_WAL, litestream opens a long-lived file descriptor on the database file with os.Open; this descriptor is required for non-OFD (POSIX flock-style) locking during sync. If the OS refuses to open the file, init fails with this wrapped error. Since litestream only initializes existing databases (it returns early if the file does not exist), failure usually means a filesystem-level problem rather than a missing file.

Source

Thrown at db.go:1060

		return err
	}
	db.dirInfo = fi

	dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(%d)&_pragma=wal_autocheckpoint(0)",
		db.path, db.BusyTimeout.Milliseconds())

	if db.db, err = sql.Open("sqlite", dsn); err != nil {
		return err
	}

	// Set PERSIST_WAL to prevent WAL file removal when database connections close.
	if err := db.setPersistWAL(ctx); err != nil {
		return fmt.Errorf("set PERSIST_WAL: %w", err)
	}

	// Open long-running database file descriptor. Required for non-OFD locks.
	if db.f, err = os.Open(db.path); err != nil {
		return fmt.Errorf("open db file descriptor: %w", err)
	}

	// Ensure database is closed if init fails.
	// Initialization can retry on next sync.
	defer func() {
		if err != nil {
			_ = db.releaseReadLock()
			db.db.Close()
			db.f.Close()
			db.db, db.f = nil, nil
		}
	}()

	// Enable WAL and ensure it is set. New mode should be returned on success:
	// https://www.sqlite.org/pragma.html#pragma_journal_mode
	var mode string
	if err := db.db.QueryRowContext(ctx, `PRAGMA journal_mode = wal;`).Scan(&mode); err != nil {
		return err

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Check file permissions and the litestream process user: `ls -l /path/to/db` and ensure read access (chmod/chown or run litestream with the right user)
  2. Check the open-file limit: `ulimit -n` (or the container's nofile rlimit) and raise it if many databases are monitored
  3. Confirm the file was not deleted/replaced between startup steps; point litestream at a stable path
  4. Inspect dmesg/audit logs for SELinux or AppArmor denials if permissions look correct

Example fix

// before: run container as arbitrary user against root-owned db
// after: grant access
//   chown litestream:litestream /var/lib/app/app.db
//   # or in Dockerfile
//   USER litestream
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(dbPath)
if err != nil { return err }
if fi.IsDir() { return errors.New("path is a directory") }
f, err := os.OpenFile(dbPath, os.O_RDONLY, 0)
if err != nil { return fmt.Errorf("pre-flight open failed: %w", err) }
f.Close()

Try / catch

if err := db.init(ctx); err != nil {
    var pe *fs.PathError
    if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EMFILE) {
        // raise RLIMIT_NOFILE and retry
    }
    log.Printf("init failed: %v", err)
}

Prevention

When it happens

Trigger: DB.init on an existing database file: os.Open(db.path) returns an error — permission denied on the file, a race where the file is unlinked between the earlier os.Stat and os.Open, too many open file descriptors (EMFILE), or the path is a directory/unreadable special file.

Common situations: Running litestream as a user without read access to the SQLite file (e.g. container running as non-root against a root-owned db); ulimit -n too low in containers spawning many databases; the database file being rotated/deleted by another process during startup; SELinux/AppArmor blocking file access.

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


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