SQLite is often used in applications that need a lightweight database without running a separate database server. It works well for desktop software, mobile applications, local services, embedded systems, and applications that store configuration or operational data.
When an application enables Write-Ahead Logging (WAL), SQLite changes how it records database updates. Instead of writing every transaction directly into the main database file, it initially records changes in a separate write-ahead log file. This approach improves concurrency by allowing readers and writers to work together more effectively.
However, developers sometimes notice that a database directory contains files such as app.db, app.db-wal, and app.db-shm, with the WAL file growing much larger than expected.
This behavior is often related to SQLite checkpointing.
Understanding how WAL checkpoints work helps developers control disk usage, investigate long-running transactions, and prevent database files from growing unnecessarily during normal application activity.
Understanding SQLite Write-Ahead Logging
SQLite supports multiple journaling modes. The traditional rollback journal records information needed to undo an incomplete transaction. WAL mode takes a different approach by recording changes in a separate log before those changes are incorporated into the main database file.
To enable WAL mode, execute the following SQL statement:
PRAGMA journal_mode = WAL;
SQLite returns the selected journal mode. When WAL is successfully enabled, the result is wal.
In WAL mode, a database directory typically contains three files:
File | Purpose |
|---|
app.db
| The main database containing database pages. |
app.db-wal
| The write-ahead log containing committed changes that have not necessarily been checkpointed into the main database. |
app.db-shm
| A shared-memory coordination file used by connections to coordinate WAL access. |
The WAL file is not simply a temporary file containing uncommitted transactions. It can contain committed transactions that are already visible to readers but have not yet been copied into the main database file.
The shared-memory file supports coordination between connections. SQLite can recreate it when necessary, so applications should not treat it as permanent business data.
Why WAL Improves Concurrency
In rollback-journal mode, a writer may need to prevent readers from accessing the database during parts of a transaction.
WAL mode allows readers to continue reading an earlier database snapshot while a writer appends new changes to the WAL. This reduces reader-writer interference.
For example, consider an application that handles database reads while processing incoming events:
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS Orders
(
Id INTEGER PRIMARY KEY,
CustomerName TEXT NOT NULL,
Status TEXT NOT NULL
);
INSERT INTO Orders (CustomerName, Status)
VALUES ('Alex', 'Pending');
A writer can commit a new order while a reader continues to access a consistent snapshot of the database.
However, WAL mode does not mean that SQLite supports unlimited simultaneous writers. SQLite still serializes write transactions, so applications must account for write contention when many operations attempt to modify the same database.
What Is a WAL Checkpoint?
A WAL checkpoint transfers database pages recorded in the WAL back into the main database file.
Without checkpointing, the WAL could continue accumulating database changes even after transactions have committed.
A checkpoint allows SQLite to incorporate eligible WAL content into the main database. Depending on the checkpoint mode and the state of active readers and writers, SQLite may not be able to complete all the work in a single attempt.
The process can be summarized as follows:
A transaction modifies database pages.
SQLite records the changes in the WAL.
The transaction commits, making its changes visible according to SQLite's transaction rules.
A checkpoint copies eligible changes from the WAL into the main database.
SQLite can eventually reset or reuse the WAL when conditions permit.
The important distinction is that committing a transaction and checkpointing the WAL are separate operations.
A successful commit does not guarantee that the corresponding changes have already been copied into the main database file.
Why the WAL File Can Grow
Suppose an application repeatedly updates records:
UPDATE Orders
SET Status = 'Processing'
WHERE Id = 1;
The application later changes the same record again:
UPDATE Orders
SET Status = 'Completed'
WHERE Id = 1;
SQLite records the relevant page changes in the WAL. As more transactions commit, the WAL can accumulate additional frames.
A checkpoint can copy eligible frames into the main database. However, the physical WAL file does not necessarily shrink immediately after every checkpoint.
SQLite commonly reuses space in the WAL instead of truncating the file after each checkpoint. This behavior avoids repeated file allocation and truncation overhead.
Consequently, a large WAL file does not automatically indicate a database problem.
The important questions are whether checkpointing is making progress, whether active readers are preventing progress, and whether the file continues growing beyond the application's expected workload.
Understanding SQLite Checkpoint Modes
SQLite provides several checkpoint modes. Each mode determines how aggressively SQLite attempts to transfer WAL content into the main database.
The primary modes are PASSIVE, FULL, RESTART, and TRUNCATE.
PASSIVE Checkpoint
A passive checkpoint copies as many eligible WAL frames as possible without waiting for readers or writers to become available.
PRAGMA wal_checkpoint(PASSIVE);
This mode is useful when checkpointing should interfere as little as possible with application activity.
SQLite does not wait for blocking readers to finish. As a result, the checkpoint may complete only part of the available work.
Use passive checkpoints when monitoring WAL activity or when you want checkpointing to make progress without deliberately waiting for other connections.
FULL Checkpoint
A full checkpoint attempts to checkpoint the entire WAL. It can wait for readers and writers to reach states that allow the operation to proceed.
PRAGMA wal_checkpoint(FULL);
This mode can be useful when an application has a suitable maintenance window.
However, a full checkpoint can introduce delays if other connections are actively using the database. Applications should avoid assuming that this operation will always finish immediately.
RESTART Checkpoint
A restart checkpoint attempts to complete checkpointing and then waits until readers no longer need the WAL so that the next writer can restart the WAL from the beginning.
PRAGMA wal_checkpoint(RESTART);
This mode is useful when the application wants to prepare the WAL for reuse.
It does not necessarily mean that the operating system will immediately report a smaller WAL file. The file may remain allocated for reuse.
TRUNCATE Checkpoint
A truncate checkpoint attempts to complete checkpointing and then truncate the WAL file to zero bytes.
PRAGMA wal_checkpoint(TRUNCATE);
This is the most direct option when the objective is to reclaim the WAL file's allocated length.
However, it may need to wait for other database activity. If a reader or writer prevents the checkpoint from completing, the operation may not achieve the expected result.
Use this mode selectively, such as during a maintenance operation, rather than forcing it after every transaction.
Checkpoint Mode Comparison
Mode | Main behavior | Typical use |
|---|
PASSIVE
| Checkpoints eligible frames without waiting for blocking readers or writers. | Routine monitoring and low-interference checkpointing. |
FULL
| Attempts to checkpoint the WAL completely, potentially waiting for other connections. | Maintenance operations requiring a more complete checkpoint. |
RESTART
| Completes checkpointing and waits for readers to release the WAL so it can be restarted. | Preparing the WAL for reuse. |
TRUNCATE
| Attempts to checkpoint the WAL and reduce its file size to zero. | Reclaiming WAL file space when database activity permits. |
Checkpoint modes are not interchangeable. Choose the mode based on whether the application prioritizes low interference, checkpoint completion, WAL reuse, or immediate file-size reduction.
How Long-Running Readers Prevent Checkpoint Progress
One of the most common reasons for persistent WAL growth is a long-running read transaction.
In WAL mode, readers use a snapshot of the database. SQLite must preserve the WAL content required by active readers, because those readers may still need to access older versions of database pages.
Consider this transaction:
BEGIN;
SELECT Id, CustomerName, Status
FROM Orders
WHERE Status = 'Pending';
If the transaction remains open, it may continue holding a read snapshot even after the application has finished processing the returned rows.
Meanwhile, another connection can continue committing updates:
UPDATE Orders
SET Status = 'Processing'
WHERE Id = 10;
UPDATE Orders
SET Status = 'Completed'
WHERE Id = 11;
These writes can append additional frames to the WAL.
If the reader's snapshot prevents checkpoint progress, the WAL may continue growing as new transactions commit.
The exact behavior depends on the transactions and WAL state. Not every active reader blocks every checkpoint, but a reader that retains an old snapshot can prevent SQLite from fully checkpointing the WAL.
Common Causes of Long-Running Read Transactions
Typical causes include:
Starting a transaction and forgetting to commit or roll it back.
Processing a large result set while keeping the transaction open.
Waiting for an HTTP request or external service inside a database transaction.
Reusing a database connection without properly disposing of transaction objects.
Running background jobs that perform lengthy processing while retaining a read snapshot.
For example, this application pattern is risky:
using var transaction = connection.BeginTransaction();
var orders = connection.Query<Order>(
"""
SELECT Id, CustomerName, Status
FROM Orders
""",
transaction: transaction
);
// Expensive processing occurs while the transaction remains open.
await SendOrdersToExternalServiceAsync(orders);
transaction.Commit();
The transaction remains open while the application waits for an external service. During that period, other connections may write to the database, while the old read snapshot can restrict checkpoint progress.
A better approach is to retrieve the required data, finish the database transaction, and then perform external processing.
For example:
List<Order> orders;
using (var connection = new SqliteConnection(connectionString))
{
connection.Open();
orders = connection.Query<Order>(
"""
SELECT Id, CustomerName, Status
FROM Orders
WHERE Status = 'Pending'
""").ToList();
}
// Database work has completed before external processing begins.
await SendOrdersToExternalServiceAsync(orders);
This example uses Dapper with Microsoft.Data.Sqlite. It assumes that the query does not need to participate in a larger explicit transaction. If the application requires transactional consistency across multiple database operations, keep those operations in a transaction, but avoid holding that transaction open during unrelated network or CPU-intensive work.
Monitoring WAL Checkpoint Progress
SQLite provides a checkpoint status interface through the wal_checkpoint pragma.
Run:
PRAGMA wal_checkpoint(PASSIVE);
The result contains three integer values:
A status value indicating whether the checkpoint was blocked or encountered an error condition.
The number of WAL frames.
The number of frames that were checkpointed.
For example, a result might look like this:
0 | 1200 | 1200
This indicates that the checkpoint was not reported as blocked and that the reported WAL frames were checkpointed.
A result such as:
0 | 1200 | 350
indicates that 350 of the 1,200 reported frames were checkpointed during that operation. The remaining frames may require further checkpointing or may be constrained by active database activity.
These values are a snapshot of checkpoint status, not a complete history of WAL growth. Repeated measurements are more useful than a single result.
Also, the pragma's status value should not be interpreted as a universal guarantee that every aspect of database health is normal. Checkpoint results must be considered alongside application errors, file sizes, transaction behavior, and SQLite's return codes.
Monitoring With C#
The following example runs a passive checkpoint and reads its reported values using Microsoft.Data.Sqlite:
using Microsoft.Data.Sqlite;
var connectionString = "Data Source=app.db";
using var connection = new SqliteConnection(connectionString);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "PRAGMA wal_checkpoint(PASSIVE);";
using var reader = command.ExecuteReader();
if (reader.Read())
{
var status = reader.GetInt32(0);
var walFrames = reader.GetInt32(1);
var checkpointedFrames = reader.GetInt32(2);
Console.WriteLine($"Checkpoint status: {status}");
Console.WriteLine($"WAL frames: {walFrames}");
Console.WriteLine($"Checkpointed frames: {checkpointedFrames}");
if (walFrames > checkpointedFrames)
{
Console.WriteLine(
"Some WAL frames were not checkpointed during this operation.");
}
}
This is a diagnostic example, not a complete checkpoint management service. A production application should also handle database exceptions, record measurements over time, and avoid running checkpoint operations too frequently.
If a checkpoint reports incomplete progress repeatedly, investigate active transactions and database access patterns before attempting increasingly aggressive checkpoints.
Configuring Automatic Checkpointing
SQLite normally performs automatic checkpointing when the WAL reaches a configured threshold. The default automatic checkpoint threshold is commonly 1,000 WAL pages.
The threshold is measured in pages, not bytes. Its approximate size in bytes depends on the database page size.
For example, if the page size is 4,096 bytes, 1,000 pages correspond to approximately 4 MB of WAL page content. The actual file size can differ because of WAL headers, frame headers, and other file details.
You can inspect the current database page size:
PRAGMA page_size;
You can configure the automatic checkpoint threshold:
PRAGMA wal_autocheckpoint = 1000;
This configures the connection's automatic checkpoint threshold. The value is expressed in pages.
A lower threshold may trigger checkpointing more frequently, potentially keeping WAL growth smaller under some workloads. However, frequent checkpointing can increase write-related overhead.
A higher threshold can reduce checkpoint frequency but allow the WAL to grow larger before automatic checkpointing is triggered.
Important Connection-Level Considerations
SQLite settings do not all have the same scope. Some settings are stored in the database file, while others apply to an individual connection.
For example, journal_mode = WAL persists as the database's journal mode. By contrast, wal_autocheckpoint is configured for a connection.
If an application creates multiple connections, do not assume that executing PRAGMA wal_autocheckpoint on one connection automatically configures every other connection.
Configure connection-specific settings consistently where required, and verify the behavior of the SQLite library and connection pool used by the application.
Common Mistakes That Make WAL Growth Worse
Assuming That Every Large WAL File Indicates a Leak
A large WAL file can be normal after a burst of writes. SQLite may retain the file for reuse even after checkpointing.
Instead of relying on file size alone, monitor whether the file keeps growing, whether checkpoints progress, and whether disk usage exceeds operational limits.
Forcing a Truncate Checkpoint After Every Write
Calling PRAGMA wal_checkpoint(TRUNCATE) after every insert or update can introduce unnecessary coordination and delays.
A checkpoint is not required after each committed transaction. Let SQLite's automatic checkpointing handle ordinary workloads unless measurements show that a different strategy is necessary.
Keeping Transactions Open During External Operations
Database transactions should generally cover the database operations that require transactional consistency. Waiting for remote APIs, processing large files, or performing lengthy business logic inside a transaction can unnecessarily extend snapshot lifetimes or retain locks.
Complete database work before starting unrelated operations whenever the application's consistency requirements allow it.
Deleting the WAL File Manually
Do not manually delete an active -wal file to reduce disk usage.
The WAL can contain committed database changes that have not yet been incorporated into the main database file. Removing it while the database is in use can cause data loss or database corruption.
Use SQLite's checkpointing mechanisms and supported connection-management procedures instead.
Treating the Shared-Memory File as the Main Problem
The -shm file is used for coordination between WAL connections. Its presence does not itself indicate that checkpointing has failed.
Investigate the WAL file, checkpoint results, transaction lifetimes, and actual storage consumption rather than deleting SQLite's supporting files.
Advantages and Disadvantages of WAL Checkpointing
Advantages
Keeps committed changes from accumulating indefinitely in the WAL under normal conditions.
Allows SQLite to transfer database pages into the main database file.
Supports continued WAL reuse, reducing the need to repeatedly allocate new storage.
Provides different checkpoint modes for routine operation and maintenance.
Helps applications manage disk usage and diagnose transaction-related bottlenecks.
Disadvantages
Checkpoints may be delayed or incomplete when active readers require older database snapshots.
Aggressive checkpointing can interfere with application responsiveness.
A successful checkpoint does not always cause an immediate reduction in physical WAL file size.
Monitoring requires understanding both frame counts and file-system measurements.
Applications must coordinate maintenance with active database connections when truncation is required.
Troubleshooting Checklist for Production Applications
When a SQLite WAL file keeps growing, investigate the problem systematically.
Confirm that the database is operating in WAL mode by running PRAGMA journal_mode;.
Record the sizes of the main database, WAL, and shared-memory files over time.
Run PRAGMA wal_checkpoint(PASSIVE); and inspect the returned status and frame counts.
Check for long-running transactions, unfinalized statements, and connections that remain open unnecessarily.
Review whether the application performs lengthy external operations while a database transaction is active.
Verify the automatic checkpoint configuration on the connections used by the application.
Check available disk space and determine whether WAL growth corresponds to a temporary write burst or a persistent pattern.
If the application needs to reclaim WAL file space, schedule an appropriate checkpoint operation when database activity permits it.
Test the checkpoint strategy under realistic concurrent read and write workloads before deploying configuration changes.
If the WAL remains large despite repeated successful checkpointing, determine whether SQLite is retaining the file for reuse. A stable, reusable WAL file is different from one that continues accumulating uncheckpointed changes.
Best Practices for SQLite WAL Management
For applications using SQLite in production, follow these practices:
Keep read and write transactions as short as practical.
Dispose of database commands, readers, transactions, and connections correctly.
Avoid waiting for external services while holding database transactions open.
Use automatic checkpointing as the baseline rather than forcing checkpoints after every write.
Monitor checkpoint progress and file-size trends together.
Choose PASSIVE, FULL, RESTART, or TRUNCATE according to the operational requirement.
Do not delete WAL or shared-memory files manually while the database is active.
Test checkpoint behavior with the same SQLite version, library, and connection-management strategy used in production.
Ensure that backup procedures are compatible with WAL mode and capture a consistent database state.
Conclusion
SQLite WAL checkpointing is an important part of managing database storage and concurrency. The WAL file records committed changes before they are incorporated into the main database, and checkpoints transfer eligible changes back into the database file.
When the WAL grows unexpectedly, the cause may be a high write volume, incomplete checkpoint progress, a long-running reader, or simply SQLite retaining the WAL file for reuse. File size alone is not enough to identify the problem.
Start by monitoring checkpoint results, reviewing transaction lifetimes, and checking automatic checkpoint configuration. Use aggressive checkpoint modes only when the application needs them and database activity permits them.
With the right monitoring and transaction-management practices, developers can keep SQLite storage behavior predictable without introducing unnecessary checkpoint overhead or risking database integrity.
Join the conversation! Your thoughts help the community grow.