Bypass SQLite exclusive lock š
āError: database is lockedā is not an acceptable answer
October 14, 2021 | Updated on September 4, 2026
āError: database is lockedā is not an acceptable answer
October 14, 2021 | Updated on September 4, 2026
Chrome database with rollback journal: sqlite3 'file:Web Data?mode=ro&nolock=1'.
Firefox database with WAL: sqlite3 'file:places.sqlite?immutable=1'.
Thereās a number of ways SQLite can lock a database file, and if youāre encountering a ādatabase is lockedā error, according to the internet, you have two options:
By āyouāre fuckedā I mean that your seemingly only option is to copy the whole database file and query the copy. If working off a one-time snapshot of the database work for you, awesome, problem solved:
$ echo .tables | sqlite3 places.sqlite
Error: database is locked
$ cp places.sqlite places-snapshot.sqlite
$ echo '.tables' | sqlite3 places-snapshot.sqlite
actual_table
But it seems thatās not a good enough solution for many people (including myself) and weāre desperately trying to perform read-only queries on a locked SQLite database.
This is especially useful for Firefox and Chrome SQLite files because both browsers have a bad tendency to keep them permanently locked, preventing us to access the database without closing the browser first.
In my case, I want to poll a specific table, and while technically the database is small enough that itās not a problem to copy it over and over to query it periodically, I just donāt like this idea and I believe there must be a better way.
So let me tell you the better way.
SQLite allows passing a
file: URI instead of a filename
(e.g. file:places.sqlite instead of places.sqlite), which comes with the
extra ability to pass query string parameters.
Some of those are aliases for flags you could otherwise set when opening
the connexion, for example mode=ro
is equivalent to setting SQLITE_OPEN_READONLY
and cache=private
the same as SQLITE_OPEN_PRIVATECACHE.
But we also have other parameters that have a deeper implementation that
would otherwise be inaccessible to the SQLite user (no configuration
flags for those). In particular, nolock
and immutable.
For SQLite databases using the default rollback journal, as Chrome does,
we can get away with using nolock to bypass the lock. From the
documentation:
The
nolockparameter is a boolean query parameter which if set disables file locking in rollback journal modes. This is useful for accessing a database on a filesystem that does not support locking.Caution: database corruption might result if two or more processes write to the same database and any one of those processes uses
nolock=1.
$ echo .tables | sqlite3 'Web Data'
Error: database is locked
$ echo '.tables' | sqlite3 'file:Web Data?nolock=1'
actual_table
As long as youāre only reading data, youāll be fine. You can enforce that by using:
sqlite3 'file:Web Data?mode=ro&nolock=1'
For WAL SQLite databases like used in Firefox, the nolock parameter
has no effect, which leaves us with immutable. From its
documentation:
The
immutableparameter is a boolean query parameter that indicates that the database file is stored on read-only media. Whenimmutableis set, SQLite assumes that the database file cannot be changed, and so the database is opened read-only and all locking and change detection is disabled.Caution: setting the
immutableproperty on a database file that does in fact change can result in incorrect query results and/orSQLITE_CORRUPTerrors.See also:
SQLITE_IOCAP_IMMUTABLE.
Even though SQLITE_IOCAP_IMMUTABLE is not an option per se, but a
particular characteristic of the IO device, we can force SQLite to treat
the database as if was on an read-only device by setting immutable=1,
which has the particularity of disabling all locking mechanisms,
including that of respecting existing locks.
$ echo .tables | sqlite3 places.sqlite
Error: database is locked
$ echo '.tables' | sqlite3 'file:places.sqlite?immutable=1'
actual_table
This doesnāt require creating a copy of the file that you want to query despite it being locked by another active connection!
The catch is SQLite assumes the file never changes, so it ignores the
WAL. You see the last checkpoint in the main file and not necessarily
all the latest writes. If a checkpoint runs during the query, you can also
get incorrect results and/or SQLITE_CORRUPT
as mentioned earlier.
Because of that, I would recommend opening a new connection every time you want to query the database.
Itās nice to be able to do that with the CLI, but how do we do that from
a program that uses a SQLite driver? In my case Iām using
sqlite3 with Node.js, but the
method should be very similar in your language of choice.
Because the nolock and immutable options are only available in the
URI filename format, we need to pass
this kind of URI to our driver, e.g. file:places.sqlite?immutable=1 as
opposed to places.sqlite.
The URI format is not enabled by default and you need to pass the
SQLITE_OPEN_URI
flag in order to enable it.
With sqlite3, this looks like this:
const sqlite3 = require('sqlite3')
const db = new sqlite3.Database('file:places.sqlite?immutable=1', sqlite3.OPEN_READONLY | sqlite3.OPEN_URI)
We need to precise OPEN_READONLY because OPEN_URI alone is not a
valid mode, and by passing an explicit mode, weāre effectively
overriding the default
of SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX.
I hope you enjoyed this trick! If you find a better way to do this (especially for WAL to be able to reflect underlying database updates), please let me know, Iād love to know about it!
And as usual, keep hacking. š