tursodatabase/turso · error · SqliteException

5

5

Error message

SQLite Error 5: 'database is locked'.

What it means

The synchronous batch execution path simulates SQLite's SQLITE_BUSY (error code 5, 'database is locked'): the connection already has an open data reader and the batch contains a write statement, so executing would mutate the database while a reader holds it. The implementation waits (Thread.Sleep for the command timeout) and then throws SqliteException with code 5.

Source

Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteBatch.cs:418

            }

            mapping.Command.SetRecordsAffected(commandTotal);
            total = checked(total + commandTotal);
        }

        return total;
    }

    private SqliteCommand CreateReaderOwner()
        => new(_connection) { Transaction = _transaction };

    private void WaitForOpenReader(IReadOnlyList<ManagedSqliteStatement> statements)
    {
        if (_connection?.HasOpenReader != true || !statements.Any(SqliteCommand.IsWriteStatement))
            return;

        Thread.Sleep(TimeSpan.FromSeconds(Timeout));
        throw new SqliteException(Properties.Resources.SqliteNativeError(5, "database is locked"), 5);
    }

    private async Task WaitForOpenReaderAsync(
        IReadOnlyList<ManagedSqliteStatement> statements,
        CancellationToken cancellationToken)
    {
        if (_connection?.HasOpenReader != true || !statements.Any(SqliteCommand.IsWriteStatement))
            return;

        await Task.Delay(TimeSpan.FromSeconds(Timeout), cancellationToken).ConfigureAwait(false);
        throw new SqliteException(Properties.Resources.SqliteNativeError(5, "database is locked"), 5);
    }

    private static object? ReadScalar(DbDataReader reader)
    {
        do
        {
            if (reader.FieldCount > 0 && reader.Read())

View on GitHub (pinned to 6c72522679)

Solutions

  1. Close/dispose the open DbDataReader before executing the batch
  2. Buffer the reader results into a list, close the reader, then execute the write batch
  3. Use a separate connection for the write if concurrent read/write is required
  4. Reduce CommandTimeout is irrelevant - the wait is fixed; the fix is not holding the reader

Example fix

// before
using var reader = cmd.ExecuteReader();
while (reader.Read())
    batch.ExecuteNonQuery(); // throws: reader open
// after
var rows = new List<object[]>();
using (var reader = cmd.ExecuteReader())
    while (reader.Read()) { /* buffer rows */ }
foreach (var _ in rows)
    batch.ExecuteNonQuery();
Defensive patterns

Strategy: try-catch

Validate before calling

if (connection.HasOpenReader && batch.BatchCommands.Any(c => /* IsWriteStatement */ c.CommandText.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase)))
    throw new InvalidOperationException("Close the open reader before executing a write batch.");

Try / catch

try { batch.ExecuteNonQuery(); }
catch (SqliteException ex) when (ex.SqliteErrorCode == 5)
{
    // close readers, then retry once
    batch.ExecuteNonQuery();
}

Prevention

When it happens

Trigger: Executing SqliteBatch.ExecuteNonQuery while another SqliteCommand/reader on the same managed connection still has open results (HasOpenReader == true) and the batch contains INSERT/UPDATE/DELETE/DDL (IsWriteStatement).

Common situations: Iterating a DataReader from one command and executing a batch write on the same connection inside the loop; forgetting to close/dispose a reader before writing; draining a SELECT into memory first is required in single-connection managed mode.

Related errors


AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-09-06). Data as JSON: /api/errors/e085af5604a0c0c2. Report an issue: GitHub.