DEV Community

Cover image for Unlocking and Journaling in SQLite
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

Unlocking and Journaling in SQLite

Hello, I'm Maneshwar. I'm working on FreeDevTools online currently building "one place for all dev tools, cheat codes, and TLDRs" — a free, open-source hub where developers can quickly find and use tools without any hassle of searching all over the internet.

In yesterday’s post, we followed the lock escalation path inside SQLite, all the way up to EXCLUSIVE locks, and saw how sqlite3OsLock carefully coordinates process wide and connection level state without ever letting native locks break isolation.

Today’s reading completes that picture in two important ways:

  1. How locks are safely released or downgraded
  2. How journaling guarantees atomicity and durability once writes begin

Together, these mechanisms close the loop on SQLite’s transaction safety model.

The sqlite3OsUnlock API: Controlled Lock Downgrades

Lock acquisition in SQLite is asymmetric — locks can only be strengthened using sqlite3OsLock. Releasing or weakening a lock is handled separately by the sqlite3OsUnlock API.

The function signature is:

int sqlite3OsUnlock(unixFile *id, int locktype);
Enter fullscreen mode Exit fullscreen mode

On Unix platforms, this maps to posixUnlock in os_unix.c.
image

A key design constraint is that sqlite3OsUnlock can only reduce lock strength to: SHARED, or NOLOCK

Any attempt to increase lock strength must go through sqlite3OsLock.

Connection-Level vs Process-Level State (Again)

Just like lock acquisition, unlocking compares two states:

  • id->eFileLock → lock held by this specific database connection
  • id->inodeinfo->eFileLock → strongest lock held by the process on this inode

This distinction ensures that one connection cannot accidentally undo another’s locks and process-wide invariants remain intact

Early Exit: Nothing to Do

If the connection already holds a lock weaker than or equal to the requested downgrade

SQLite immediately returns success.

This avoids unnecessary work and keeps unlock paths fast.

Lock Downgrades from Stronger Modes

If the connection currently holds a lock stronger than SHARED, SQLite enters a lock downgrade path.

For example:

  • EXCLUSIVE → SHARED
  • RESERVED → SHARED

In this case:

  • a read lock is placed on the SHARED_BYTES region
  • write locks on the PENDING and RESERVED regions are cleared

This ensures that readers may safely proceed, writers are still excluded and no process-wide invariants are violated

Image

Unlocking to NOLOCK: Releasing the File Completely

When the requested lock type is NOLOCK, SQLite is signaling that this connection is done with the file.

At this stage:

  1. The shared-lock counter nShared in unixinodeinfo is decremented
  2. If nShared reaches zero:
    • all native locks on the file are cleared
  3. The process-wide lock counter nLock is decremented
  4. If nLock reaches zero:
    • all lazy-close file descriptors are finally closed

This step is crucial: it ties together unlocking with lazy file closing, ensuring that file descriptors are only released when no thread still depends on the locks.

Why Unlocking Is Just as Careful as Locking

Unlike many databases, SQLite cannot rely on the OS to “do the right thing” when unlocking. A premature unlock or close could:

  • silently drop another transaction’s lock
  • allow concurrent writers
  • corrupt the database

The sqlite3OsUnlock algorithm is deliberately conservative to prevent these outcomes.

Journaling: The Other Half of Transaction Safety

Locks alone prevent concurrent corruption. They do not protect against crashes, power failures or aborted transactions

That responsibility belongs to journaling.

A journal is a repository of recovery information that allows SQLite to restore the database to a consistent state.

SQLite uses rollback journaling, not redo logging.

The Rollback Journal

For each database file, SQLite maintains one rollback journal (except for in-memory databases).

Key properties:

  • One write-transaction at a time
  • Journal created at the start of a write-transaction
  • Journal deleted (or truncated) when the transaction completes

The rollback journal stores only undo information, never redo data.
Image

Transient vs Retained Journals

By default, SQLite uses transient journaling:

  • journal file created per transaction
  • deleted on commit or rollback

This behavior can be modified using:

PRAGMA journal_mode;
Enter fullscreen mode Exit fullscreen mode

Options include:

  • truncate – keep file, truncate contents
  • persist – keep file, invalidate header
  • memory – journal stored entirely in RAM
  • off – journaling disabled (dangerous)

Each option trades durability, performance, and crash safety differently.

Idempotent Undo: A Powerful Property

Because SQLite logs full page images:

  • undo is done by blindly copying pages back
  • undo operations are idempotent
  • no compensating log records are needed

If recovery is interrupted and restarted:

  • repeating undo causes no harm

This simplicity is one of SQLite’s greatest strengths.

Handling Database Growth

If a transaction adds new pages:

  • there is no old value to log

SQLite handles this by:

  • recording the original database size in the journal header
  • truncating the database file back to that size on rollback

Tracking Journaled Pages

SQLite keeps an in-memory bitmap to track which pages have already been journaled.

Benefits:

  • prevents duplicate logging
  • memory usage proportional to number of modified pages
  • negligible overhead for small transactions

Log Optimization: Freelist Pages

When a freelist leaf page is reused:

  • its contents are considered garbage
  • no useful old value exists

SQLite optimizes by skipping journaling for such pages.

A Critical Warning: No Database Aliasing

SQLite assumes one database file → one canonical name

Using hard links, symbolic links or renamed database files can result in:

  • multiple journal files for the same database
  • missed recovery steps
  • irreversible corruption

Similarly renaming a database without renaming its journal or interacting with a master journal

is extremely dangerous.

You have been warned — severely.

Where This Takes Us Next

At this point, we have all the building blocks:

  • lock acquisition
  • lock release
  • undo logging
  • multi-database coordination

The next natural step is to study:

  • logging protocol
  • commit protocol
  • asynchronous transactions
  • lazy commit

That’s where SQLite turns all of this machinery into durable, crash-safe commits.

My experiments and hands-on executions related to SQLite will live here: lovestaco/sqlite

References:

SQLite Database System: Design and Implementation. N.p.: Sibsankar Haldar, (n.d.).

FreeDevTools

👉 Check out: FreeDevTools

Any feedback or contributors are welcome!

It’s online, open-source, and ready for anyone to use.

⭐ Star it on GitHub: freedevtools

Top comments (0)