| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Microsoft.Data.Sqlite.SqliteException: SQLite Error 5: 'database is locked'
This is the fix. One line change, no other modifications to your code:
// Before:
options.UseSqlite("Data Source=app.db");
// After — eliminates SQLITE_BUSY, serializes writes, enables WAL-mode parallel reads:
options.UseSqliteWithConcurrency("Data Source=app.db");| Error / Exception | Root Cause | Fix |
|---|---|---|
| SQLite Error 5: 'database is locked' (SQLITE_BUSY) | Multiple writers contending simultaneously | Channel-based write queue — one writer at a time, all others park efficiently in FIFO order |
| SQLite Error 5: 'database is locked' (extended code 517, SQLITE_BUSY_SNAPSHOT) | WAL read snapshot became stale mid-transaction | BEGIN IMMEDIATE upgrade + full operation restart on snapshot staleness |
| InvalidOperationException: A second operation was started on this context instance | DbContext shared across concurrent tasks | IDbContextFactory<T> registration — one independent context per concurrent flow |
| Bulk inserts slow or running out of memory on large datasets | Linear SaveChanges(), unbounded ChangeTracker growth per batch | BulkInsertOptimizedAsync — batched writes, ChangeTracker.Clear() per batch, WAL-optimized transactions |
See Troubleshooting SQLITE_BUSY errors for a deep-dive on each error code.
| Problem with Standard EF Core + SQLite | Solution |
|---|---|
| SQLITE_BUSY / database is locked under concurrent writes | Channel-based write queue serializes all writes — zero contention errors |
| SQLITE_BUSY_SNAPSHOT mid-transaction | Automatic BEGIN IMMEDIATE upgrade prevents stale read snapshots |
| Slow bulk inserts | BulkInsertOptimizedAsync — typically 5–10x faster via batching and PRAGMA tuning |
| Read contention during writes | WAL mode enabled automatically — reads never block behind writes |
| Manual retry logic | Built-in exponential backoff with full jitter for all SQLITE_BUSY* variants |
| DbContext not thread-safe | IDbContextFactory<T> wiring and ThreadSafeSqliteContext<T> base class |
# .NET CLI
dotnet add package EntityFrameworkCore.Sqlite.Concurrency
# Package Manager Console
Install-Package EntityFrameworkCore.Sqlite.ConcurrencyOne DbContext per HTTP request. ASP.NET Core scopes handle thread safety at the request level.
// Program.cs
builder.Services.AddConcurrentSqliteDbContext<AppDbContext>("Data Source=app.db");
// Inject normally — concurrent writes across requests are serialized automatically
public class PostsController : ControllerBase
{
private readonly AppDbContext _db;
public PostsController(AppDbContext db) => _db = db;
[HttpPost]
public async Task<IActionResult> Create(Post post)
{
_db.Posts.Add(post);
await _db.SaveChangesAsync(); // thread-safe across concurrent requests
return Ok();
}
}DbContext is not thread-safe — never share one instance across concurrent tasks. Use IDbContextFactory<T> to give each concurrent flow its own independent context. Writes are still serialized at the SQLite level automatically.
// Program.cs
builder.Services.AddConcurrentSqliteDbContextFactory<AppDbContext>("Data Source=app.db");
// Background service — each task gets its own context
public class ReportService
{
private readonly IDbContextFactory<AppDbContext> _factory;
public ReportService(IDbContextFactory<AppDbContext> factory) => _factory = factory;
public async Task ProcessAllAsync(IEnumerable<int> ids, CancellationToken ct)
{
var tasks = ids.Select(async id =>
{
await using var db = _factory.CreateDbContext(); // one context per task
var item = await db.Items.FindAsync(id, ct);
item.ProcessedAt = DateTime.UtcNow;
await db.SaveChangesAsync(ct); // writes serialized — no "database is locked"
});
await Task.WhenAll(tasks); // all complete without SQLITE_BUSY
}
}See Concurrent EF Core patterns for the full pattern guide.
// 10,000 records — batched in chunks of 1,000, ChangeTracker cleared between batches
await context.BulkInsertOptimizedAsync(records);
// Or with explicit retry control
await context.ExecuteWithRetryAsync(async ctx =>
{
foreach (var record in records)
ctx.Records.Add(record);
}, maxRetries: 5);builder.Services.AddConcurrentSqliteDbContext<AppDbContext>(
"Data Source=app.db",
options =>
{
options.BusyTimeout = TimeSpan.FromSeconds(30);
options.MaxRetryAttempts = 3;
options.CommandTimeout = 300;
options.WalAutoCheckpoint = 1000;
options.SynchronousMode = SqliteSynchronousMode.Normal;
options.UpgradeTransactionsToImmediate = true;
options.WriteQueueCapacity = null; // null = unbounded
});| Option | Default | Description |
|---|---|---|
| BusyTimeout | 30 s | PRAGMA busy_timeout — SQLite retries lock acquisition internally before surfacing SQLITE_BUSY. |
| MaxRetryAttempts | 3 | Application-level retries after SQLITE_BUSY*, with exponential backoff and full jitter. |
| CommandTimeout | 300 s | EF Core SQL command timeout. |
| WalAutoCheckpoint | 1000 pages | PRAGMA wal_autocheckpoint — triggers passive checkpoint after this many WAL frames (~4 MB). Set to 0 to disable. |
| SynchronousMode | Normal | PRAGMA synchronous — durability vs. write-speed. Normal is recommended for WAL mode. |
| UpgradeTransactionsToImmediate | true | Rewrites BEGIN to BEGIN IMMEDIATE to prevent SQLITE_BUSY_SNAPSHOT mid-transaction. |
| WriteQueueCapacity | null | Maximum pending writes before callers block. null = accept without limit. Set a value for back-pressure on high-volume producers. |
Note: Cache=Shared in the connection string is incompatible with WAL mode and will throw ArgumentException at startup. Connection pooling (Pooling=true) is enabled automatically and is the correct alternative.
Q: What is the Channel-based write queue? A: Since v10.1.0, write serialization uses Channel<IWriteRequest> with a single background writer instead of SemaphoreSlim. Under high concurrency, SemaphoreSlim wakes all N waiters simultaneously when the lock releases — N-1 immediately re-sleep (CPU churn, unfair scheduling, thundering herd). A Channel<T> parks callers in FIFO order and drains them one at a time. No API change is needed — existing code gets the benefit automatically.
Q: What does SQLITE_BUSY_SNAPSHOT (error code 517) mean? A: This is a specific variant of SQLITE_BUSY (extended code 5 | (2 << 8) = 517) that fires when a connection's WAL read snapshot became stale after another writer committed mid-transaction. Retrying the same statement produces the same error — the only correct fix is to roll back and restart the entire operation so all reads are re-queried against the current snapshot. This package implements that correctly.
Q: Does this work with netstandard 2.0? A: The connection layer (SqliteConnectionEnhancer, SqliteWriteQueue, SqliteConcurrencyOptions) targets netstandard2.0. EF Core-dependent APIs (UseSqliteWithConcurrency, ThreadSafeSqliteContext<T>, AddConcurrentSqliteDbContext<T>) require net10.0 because EF Core 10 requires it.
Q: How much overhead does write serialization add? A: Sub-millisecond. Enqueueing to a Channel and awaiting a TaskCompletionSource adds microseconds per write — negligible compared to actual SQLite I/O. WAL-mode PRAGMA tuning typically more than compensates on read-heavy workloads.
Q: Does this work with my existing DbContext, models, and LINQ queries? A: Yes. Change UseSqlite to UseSqliteWithConcurrency. Everything else compiles and runs unchanged.
Q: Does this work on network filesystems? A: No. SQLite WAL mode requires all connections to be on the same physical host. Do not point the database at an NFS, SMB, or other network-mounted path. Use a client/server database (PostgreSQL, SQL Server) for multi-host deployments.
Q: Why do I need IDbContextFactory for concurrent workloads? A: EF Core's DbContext is not thread-safe by design — it tracks entity state per instance and cannot be shared across concurrent tasks. IDbContextFactory<T> creates an independent context per concurrent flow. Writes from all contexts are serialized at the SQLite level by the write queue.
Measured with BenchmarkDotNet v0.14.0 on .NET 10.0.2 / Windows 11 / Intel i7-13700K:
| Benchmark | Standard EF Core | This Package | Ratio |
|---|---|---|---|
| BulkInsertOptimizedAsync — 1,000 entities | 4,500 ms | 28 ms | ~161x faster |
| Concurrent writes (50 tasks, Task.WhenAll) | throws SQLITE_BUSY | completes cleanly | ∞ |
Bulk insert: baseline calls SaveChangesAsync() per entity (1,000 individual transactions); package uses a single batched write. Full numbers and reproduce command: docs/performance-guide.md.
Reproduce: dotnet run -c Release --project EFCore.Sqlite.Concurrency.Benchmarks -- --job short
MIT License — free for commercial use, open-source projects, and enterprise applications.
| Back | FazBrowse Home | New Git URL |