Article illustration 1

For years, Jellyfin relied on SQLite as its embedded database engine—a pragmatic choice for its simplicity and portability. But as the open-source media server scaled, it encountered a sinister foe: sporadic database is locked errors during transactions. These failures occurred unpredictably across operating systems and hardware, vanishing during debugging yet crippling real-world deployments. The root? SQLite's fundamental design: a single-writer constraint that demands exclusive file access during writes.

The Concurrency Conundrum

SQLite's Write-Ahead Logging (WAL) mode offers partial relief by queuing writes in a separate journal. Yet under heavy load—like Jellyfin's bug-induced “exponential overscheduling” of library scans—WAL buckled. Thousands of concurrent write requests overwhelmed SQLite's timeout safeguards, triggering crashes. "This issue haunted us because it wasn't reproducible on demand," explains Jellyfin contributor JPVenson. "We needed a holistic fix."

Enter EF Core Interceptors

Jellyfin's migration to Entity Framework Core proved pivotal. By leveraging EF Core's interceptor pipeline, the team injected concurrency control directly into database operations without polluting application logic. The solution? Three configurable locking strategies:

  1. No-Lock: Default behavior for low-risk environments.
  2. Optimistic Locking: Retry failed writes using Polly resilience policies:
// Simplified retry logic for SQLite busy errors
policy.Handle<SqliteException>(ex => ex.SqliteErrorCode == SQLiteErrorCode.Busy)
    .WaitAndRetry(3, retryAttempt => TimeSpan.FromMilliseconds(100));
  1. Pessimistic Locking: Enforce single-writer exclusivity globally via ReaderWriterLockSlim, blocking all reads during writes for maximum stability at a performance cost.

Tradeoffs and Triumphs

Initial tests confirmed both locking modes eliminated crashes, but with clear tradeoffs. Optimistic locking minimizes overhead for most writes but risks eventual failure under contention. Pessimistic locking guarantees stability but throttles throughput. For Jellyfin, this meant empowering users to choose based on their hardware tolerance.

Critically, the interceptors require zero caller-side changes—a drop-in solution for any EF Core/SQLite application. As JPVenson notes: "When researching, I found countless reports of this error but no ready-made fix. Our implementation bridges that gap."

Beyond the Lock

Jellyfin eyes a future “Smart Locking” hybrid, dynamically switching strategies per operation. Meanwhile, the framework offers immediate relief for developers wrestling with SQLite's concurrency ghosts—proving that even “simple” databases demand sophisticated guardrails at scale.

Source: Jellyfin Blog: Solving SQLite Locking