tursodatabase/turso · error · SqliteException

5

5

Error message

SQLite Error 5: 'database is locked'.

What it means

The provider emulates SQLITE_BUSY for single-connection interleaving: if the SqliteConnection already has an open DataReader and the incoming command looks like a write (IsWriteCommand), Execute sleeps for CommandTimeout seconds and then throws SqliteException code 5 'database is locked'. A live reader pins a snapshot of rows a write would mutate, so write-while-reading on one connection is refused instead of corrupting iteration.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteCommand.cs:289

        EnsureExecutable(method);
        if (Connection!.IsManagedConnection)
        {
            return ExecuteManagedAsync(method, behavior, CancellationToken.None)
                .GetAwaiter()
                .GetResult();
        }

        if (IsEmptyCommand(CommandText))
        {
            _hasOpenReader = true;
            Connection?.ReaderOpened();
            return new SqliteDataReader(this, -1, behavior, CloseReader);
        }

        if (Connection?.HasOpenReader == true && IsWriteCommand(CommandText))
        {
            Thread.Sleep(TimeSpan.FromSeconds(CommandTimeout));
            throw new SqliteException(Properties.Resources.SqliteNativeError(5, "database is locked"), 5);
        }
        if (Connection?.IsReadOnly == true && IsWriteCommand(CommandText))
            throw new SqliteException(Properties.Resources.SqliteNativeError(8, "attempt to write a readonly database"), 8);

        var recordsAffected = 0;
        var statements = SplitStatements(CommandText);
        try
        {
            for (var i = 0; i < statements.Count; i++)
            {
                if (TryHandleFacadeStatement(statements[i], out var sql))
                    continue;

                var statement = PrepareSingleStatement(sql);
                if (TursoBindings.GetFieldCount(statement) > 0)
                {
                    _hasOpenReader = true;
                    Connection?.ReaderOpened();

View on GitHub (pinned to 6c72522679)

Solutions

  1. Fully read and dispose the DataReader before executing any write - materialize results into a list first.
  2. Buffer the rows to change, close the reader, then run one batched write.
  3. If writes must interleave with reads, use a second SqliteConnection for them.
  4. Keep CommandTimeout small so a design mistake fails fast instead of sleeping for the full timeout.

Example fix

// before
using (var reader = selectCmd.ExecuteReader()) {
    while (reader.Read()) {
        updateCmd.ExecuteNonQuery(); // same connection, reader open -> code 5
    }
}

// after
var ids = new List<long>();
using (var reader = selectCmd.ExecuteReader())
    while (reader.Read()) ids.Add(reader.GetInt64(0)); // reader disposed on exit

foreach (var id in ids)
    ExecuteUpdate(conn, id);
Defensive patterns

Strategy: retry

Validate before calling

// Materialize reads before writing on the same connection
var rows = new List<Row>();
using (var r = selectCmd.ExecuteReader())
    while (r.Read()) rows.Add(MapRow(r));
// reader is disposed here -> safe to write
foreach (var row in rows) WriteRow(conn, row);

Try / catch

try {
    updateCmd.ExecuteNonQuery();
} catch (SqliteException ex) when (ex.SqliteErrorCode == 5) {
    // dispose any open DataReaders on this connection, then retry once
    reader.Dispose();
    updateCmd.ExecuteNonQuery();
}

Prevention

When it happens

Trigger: Executing a write command on the same connection inside a using (var reader = ...) loop that is still open; issuing UPDATE/INSERT/DELETE from a callback while enumerating results; a reader left undisposed on an early-return path before a later write runs.

Common situations: Read-then-update loops (load rows, mutate, save) on one connection; progress callbacks writing during enumeration; forgotten reader disposal; large CommandTimeout values that make the failure slow as well as wrong.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20). Data as JSON: /api/errors/2e86e4636aeccedd. Report an issue: GitHub.