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
- Fully read and dispose the DataReader before executing any write - materialize results into a list first.
- Buffer the rows to change, close the reader, then run one batched write.
- If writes must interleave with reads, use a second SqliteConnection for them.
- 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
- Scope DataReaders tightly with using; never leave one open across writes.
- Materialize query results, then write.
- Set CommandTimeout low so misdesigned paths fail fast.
- Use a dedicated connection for interleaved writes.
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.