tursodatabase/turso · error · SqliteException
8
8
Error message
SQLite Error 8: 'attempt to write a readonly database'.
What it means
SqliteCommand's managed execution path throws a simulated SQLITE_READONLY (code 8, 'attempt to write a readonly database') when the connection was opened in read-only mode (Connection.IsReadOnly) and the statement list contains any write statement (INSERT/UPDATE/DELETE/DDL). The operation is rejected before touching the database.
Source
Thrown at bindings/dotnet/src/Turso.Data.Sqlite/SqliteCommand.cs:370
}
}
}
private async Task<SqliteDataReader> ExecuteManagedAsync(
string method,
CommandBehavior behavior,
CancellationToken cancellationToken)
{
EnsureExecutable(method);
var statements = GetManagedStatements();
if (Connection!.HasOpenReader && statements.Any(IsWriteStatement))
{
await Task.Delay(TimeSpan.FromSeconds(CommandTimeout), cancellationToken).ConfigureAwait(false);
throw new SqliteException(Properties.Resources.SqliteNativeError(5, "database is locked"), 5);
}
if (Connection!.IsReadOnly && statements.Any(IsWriteStatement))
{
throw new SqliteException(
Properties.Resources.SqliteNativeError(8, "attempt to write a readonly database"),
8);
}
if (statements.Count == 0)
{
_hasOpenReader = true;
Connection!.ReaderOpened();
return new SqliteDataReader(this, -1, behavior, CloseReader);
}
ValidateManagedParameterValues();
var results = new List<ManagedSqliteResult>();
var recordsAffected = 0;
var hadResultSet = false;
foreach (var statement in statements)
{
cancellationToken.ThrowIfCancellationRequested();
if (TryHandleFacadeStatement(statement.Sql, out var sql))View on GitHub (pinned to 6c72522679)
Solutions
- Reopen the connection without Mode=ReadOnly (e.g. Mode=ReadWrite or default) for write workloads
- Use two connections: a read-only one for queries and a writable one for mutations
- Check Connection.IsReadOnly (or the connection string) before issuing writes and route accordingly
- Fix file permissions if the read-only mode was unintentional (e.g. read-only mount)
Example fix
// before
var conn = new SqliteConnection("Data Source=app.db;Mode=ReadOnly");
conn.Execute("INSERT INTO t VALUES (1)"); // throws code 8
// after
var conn = new SqliteConnection("Data Source=app.db;Mode=ReadWrite");
conn.Execute("INSERT INTO t VALUES (1)"); Defensive patterns
Strategy: validation
Validate before calling
if (connection.IsReadOnly && IsWriteSql(commandText))
throw new InvalidOperationException("Cannot execute write SQL on a read-only connection."); Try / catch
try { cmd.ExecuteNonQuery(); }
catch (SqliteException ex) when (ex.SqliteErrorCode == 8)
{
// reroute to writable connection
writableConnection.Execute(cmd.CommandText);
} Prevention
- Check Mode=ReadOnly in connection strings when setting up write paths
- Maintain separate read-only and writable connection factories
- Catch SqliteErrorCode 8 to detect misrouted writes in tests
When it happens
Trigger: Opening the connection with Mode=ReadOnly in the connection string (or a read-only datasource) and executing INSERT/UPDATE/DELETE/CREATE/etc. via ExecuteNonQuery/ExecuteReader on a managed connection.
Common situations: Connection strings copied from a read-only analytics setup being reused for write endpoints; environment-specific config (prod writes allowed, CI/dataset mounted read-only); accidentally passing a read-only file path or read-only replica options.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-09-06).
Data as JSON: /api/errors/274eaad7cf7192fc.
Report an issue: GitHub.