PostgreSQL uses several internal logging mechanisms to maintain consistency, durability, and crash recovery. Two of the most important mechanisms are Write-Ahead Logging (WAL) and the Commit Log, also known as CLOG. While WAL is widely discussed, the commit log often remains less understood even though it plays a critical role in PostgreSQL’s transaction management.
This article explores what commit logs are, where they are stored, how PostgreSQL tracks transaction states internally, and how commit logs differ from WAL.
What Is a Commit Log in PostgreSQL?
In PostgreSQL, the commit log records the final status of each transaction. It tells PostgreSQL whether a transaction:
- committed successfully
- Aborted
- is still in progress
- or was sub-committed
This information allows PostgreSQL to determine the visibility of rows for other transactions.
Internally, PostgreSQL calls this system CLOG (Commit Log). In modern PostgreSQL versions, the files storing this information are located in the pg_xact directory inside the data directory.
For example, in a PostgreSQL installation, you may see something like:
cybrosys@cybrosys:~$ pg_lsclusters
Ver Cluster Port Status Owner Data directory Log file
18 main 5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log
The actual database storage directory can then be inspected.
sudo su postgres
cd /var/lib/postgresql/18/main
ls
Among the many internal directories, you will find:
postgres@cybrosys:~/18/main$ ls
base pg_commit_ts pg_logical pg_notify pg_serial pg_stat pg_subtrans pg_twophase pg_wal postgresql.auto.conf
global pg_dynshmem pg_multixact pg_replslot pg_snapshots pg_stat_tmp pg_tblspc PG_VERSION pg_xact
The pg_xact directory contains the commit logs.
cd pg_xact
ls
0000
0001
Each file in this directory stores transaction status bits for a range of transaction IDs.
How PostgreSQL Tracks Transaction Status
Every transaction in PostgreSQL receives a Transaction ID (XID). PostgreSQL keeps track of the status of these XIDs inside the commit log.
For example, you can check the current transaction ID with:
SELECT pg_current_xact_id();
Example result:
pg_current_xact_id
--------------------
109662
This means the current transaction is using transaction ID 109662.
PostgreSQL will record the final state of this transaction in the commit log when the transaction ends.
Transaction Status Values in PostgreSQL
Inside the PostgreSQL source code, transaction states are defined in clog.h. Each transaction is stored using two bits representing its status.
#define TRANSACTION_STATUS_IN_PROGRESS 0x00
#define TRANSACTION_STATUS_COMMITTED 0x01
#define TRANSACTION_STATUS_ABORTED 0x02
#define TRANSACTION_STATUS_SUB_COMMITTED 0x03
These states represent the lifecycle of a transaction.
IN_PROGRESS
The transaction has started but has not yet been completed. Other transactions may need to wait or check visibility rules.
COMMITTED
The transaction completed successfully. Any rows written by this transaction become visible to other transactions depending on their isolation level.
ABORTED
The transaction failed or was rolled back. Any changes made by this transaction must be ignored.
SUB_COMMITTED
This state occurs when a subtransaction commits but the parent transaction has not yet finished.
Because each transaction only requires two bits of storage, PostgreSQL can efficiently track millions of transaction statuses with minimal space.
Structure of the pg_xact Directory
The pg_xact directory contains files that store transaction status bits sequentially.
Each file represents a range of transaction IDs. Instead of storing one record per file, PostgreSQL packs many transaction statuses into a single page. This design significantly reduces storage overhead.
For example:
pg_xact/
0000
0001
0002
Each file contains multiple pages, and each page stores the status of many transactions.
When PostgreSQL needs to determine whether a transaction is committed or aborted, it checks the appropriate location in the pg_xact files.
Why Commit Logs Are Important
Commit logs are essential for PostgreSQL’s MVCC (Multi-Version Concurrency Control) mechanism.
When a row is inserted or updated, PostgreSQL stores the transaction ID in the tuple header. Later, when another transaction reads that row, PostgreSQL must determine whether the inserting transaction was committed.
To do this, PostgreSQL checks the commit log.
If the inserting transaction is marked committed, the row may be visible.
If it is aborted, the row must be ignored.
Without commit logs, PostgreSQL would not be able to determine tuple visibility efficiently.
WAL vs Commit Log
Although both WAL and commit logs are related to transactions, they serve completely different purposes.
WAL (Write-Ahead Log)
WAL records every change made to the database. This includes:
- tuple inserts
- Updates
- Deletes
- page modifications
- commit records
The main purpose of WAL is crash recovery and durability.
Before modifying actual data files, PostgreSQL writes the change to WAL. If the server crashes, PostgreSQL can replay WAL records to restore the database to a consistent state.
WAL files are stored in the pg_wal directory.
Commit Log (CLOG)
The commit log does not store detailed changes. Instead, it only stores the final outcome of transactions.
Its primary role is transaction visibility management.
When PostgreSQL checks whether a row should be visible to a transaction, it reads the transaction status from the commit log.
The commit log is stored in the pg_xact directory.
Relationship Between WAL and Commit Log
When a transaction commits, PostgreSQL performs several steps internally.
First, it writes a commit record to WAL. This ensures the commit is durable and can be recovered if a crash occurs.
Next, PostgreSQL updates the commit log to mark the transaction as committed.
This means WAL ensures durability, while the commit log ensures correct visibility for other transactions.
Internal Workflow of a Commit
A simplified sequence of events during a transaction commit looks like this:
- Transaction performs inserts or updates.
- WAL records are generated for these changes.
- A commit record is written to WAL.
- PostgreSQL flushes WAL to disk.
- The commit log is updated to mark the transaction as committed.
- Other transactions can now see the committed data.
This coordination ensures both durability and consistency.
Why PostgreSQL Uses a Separate Commit Log
Instead of scanning WAL to determine transaction states, PostgreSQL uses a dedicated commit log for efficiency.
Reading WAL for visibility checks would be expensive and slow. By storing transaction states in a compact structure, PostgreSQL can quickly determine whether a transaction is committed or aborted.
This design is especially important for high-concurrency systems where thousands of transactions may be active simultaneously.
Commit logs are a fundamental part of PostgreSQL’s transaction system. Stored in the pg_xact directory, they track the final state of every transaction using compact status bits.
While WAL records the actual changes made to the database and ensures durability, the commit log records whether each transaction was committed or aborted. PostgreSQL relies on this information to implement MVCC and maintain correct row visibility.
Understanding the relationship between WAL and commit logs helps developers gain deeper insight into PostgreSQL internals, particularly how transactions are managed and how PostgreSQL maintains consistency under heavy workloads.