It is safe, in the sense of not causing data races or data corruption. But you are right, SQLite itself does not allow more than one concurrent write.
There are approaches to mitigate this restriction, see, for example, this comment in issue #1179:
Unfortunately, the README is a little misleading. This library is already safe for concurrent use. However, there are a few caveats that need to be acknowledged.
SQLite will support any number of concurrent reads under any mode. It does not support a concurrent read and write to the same database file under the default rollback journal mode, but this is supported under WAL mode. It does not support concurrent writes to the same database file under any mode. It is generally recommended to use WAL. Please note that “does not support” means that it will perform these operations serially, with a wait/retry period defined by your busy handler (or the busy_timeout
pragma). If the wait period expires before the operation can proceed, then you will get SQLITE_BUSY
(aka “database is locked”).
You may have come across some suggestions to use shared cache mode to avoid SQLITE_BUSY
. Do not follow these suggestions. The SQLite authors themselves discourage using this mode under any circumstance. If you use it, it may appear to resolve the issue, but then you can start to get the similar-looking SQLITE_LOCKED
(aka “database table is locked”).
If your code is going to read and write to the database as part of the same operation, you must use a single transaction for the whole thing. Otherwise, because of how sql.DB
works, you can run into issues where the in-progress read on one connection blocks the attempted write on the other connection.
Whenever you start a transaction that might write to the database, always use BEGIN IMMEDIATE
, not the default (BEGIN DEFERRED
). Otherwise, if another connection also writes to the database in the middle of your transaction, you can get SQLITE_BUSY
without your busy handler even being triggered, due to SQLite’s need to enforce the ACID requirements of a database engine. If your transaction will definitely only read, then you should continue to use BEGIN DEFERRED
in order to allow concurrent reads to function.
My general recommendation is to have two connection pools (i.e., sql.DB
instances). One should be read-write, configured to use BEGIN IMMEDIATE
, and throttled to a single connection via SetMaxOpenConns
, SetMaxIdleConns
, SetConnMaxLifetime
, and SetConnMaxIdleTime
. The other should be read-only, configured to use BEGIN DEFERRED
(which is the default), and can be throttled to any number of connections within the limits of your system.