PostgreSQL is well known for its reliability and strong transaction consistency. However, developers working with applications such as Odoo often encounter an error similar to:
ERROR: could not serialize access due to concurrent update
At first glance, this error may appear to be a database problem. In reality, PostgreSQL intentionally prevents data corruption and maintains consistency between concurrent transactions.
This article explains the actual reason behind the concurrent update issue, what PostgreSQL is doing internally, and why this behavior is necessary.
Many applications execute multiple transactions simultaneously. When two users try to modify the same row at nearly the same time, PostgreSQL must decide how to maintain data consistency.
Under lower isolation levels, such as READ COMMITTED, PostgreSQL allows transactions to see the latest committed data.
However, under stricter isolation levels such as:
- REPEATABLE READ
- SERIALIZABLE
PostgreSQL guarantees that a transaction always works with a consistent snapshot of the database.
The concurrent update issue occurs when PostgreSQL detects that allowing a transaction to continue would violate that guarantee.
Understanding the Scenario
Consider a product inventory table:
CREATE TABLE inventory (
product_id INT,
quantity INT
);
INSERT INTO inventory VALUES (1, 10);
Current quantity:
quantity = 10
Now change the value of the parameter named log_statements in postgres to all
alter system set log_statement = 'all';
Reload the postgres conf by using the below query.
select pg_reload_conf();
Result :
pg_reload_conf
----------------
t
Verify that the value of this parameter has changed.
show log_statement;
Result:
log_statement
---------------
all
Now two transactions start almost at the same time. But start transaction 1 first, and then start transaction 2
Transaction 1 Begins
BEGIN ISOLATION LEVEL REPEATABLE READ;
Transaction 1 reads:
SELECT quantity
FROM inventory
WHERE product_id = 1;
Result:
10
At the same time, begin transaction 2
BEGIN ISOLATION LEVEL REPEATABLE READ;
Execute an update query in transaction 1
UPDATE inventory
SET quantity = 7
WHERE product_id = 1;
Don't commit this transaction. Now, in transaction 2, execute the update query with the same where clause value
UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
Now, this time, check the PostgreSQL log file.
Check the path of the PostgreSQL log file by using this command.
pg_lsclusters
Result :
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
Check the live progress of the PostgreSQL log file like this.
tail -f /var/log/postgresql/postgresql-18-main.log
Result:
2026-06-11 22:13:01.011 IST [158111] postgres@postgres LOG: statement: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:02.012 IST [158111] postgres@postgres LOG: process 158111 still waiting for ShareLock on transaction 5848 after 1000.110 ms
2026-06-11 22:13:02.012 IST [158111] postgres@postgres DETAIL: Process holding the lock: 157634. Wait queue: 158111.
2026-06-11 22:13:02.012 IST [158111] postgres@postgres CONTEXT: while updating tuple (0,3) in relation "inventory"
Now commit the transaction 1 and check the psql terminal of transaction 2 and the postgres logfile
When you commit to transaction 1, you can see an error message in transaction 2 like this.
ERROR: could not serialize access due to concurrent update
Postgres logfile at this time.
2026-06-11 22:13:01.011 IST [158111] postgres@postgres LOG: statement: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:02.012 IST [158111] postgres@postgres LOG: process 158111 still waiting for ShareLock on transaction 5848 after 1000.110 ms
2026-06-11 22:13:02.012 IST [158111] postgres@postgres DETAIL: Process holding the lock: 157634. Wait queue: 158111.
2026-06-11 22:13:02.012 IST [158111] postgres@postgres CONTEXT: while updating tuple (0,3) in relation "inventory"
2026-06-11 22:13:02.012 IST [158111] postgres@postgres STATEMENT: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:22.174 IST [157634] postgres@postgres LOG: statement: commit;
2026-06-11 22:13:22.177 IST [157634] postgres@postgres LOG: duration: 3.142 ms
2026-06-11 22:13:22.177 IST [158111] postgres@postgres LOG: process 158111 acquired ShareLock on transaction 5848 after 21165.918 ms
2026-06-11 22:13:22.177 IST [158111] postgres@postgres CONTEXT: while updating tuple (0,3) in relation "inventory"
2026-06-11 22:13:22.177 IST [158111] postgres@postgres STATEMENT: UPDATE inventory
SET quantity = 5
WHERE product_id = 1;
2026-06-11 22:13:22.178 IST [158111] postgres@postgres ERROR: could not serialize access due to concurrent update
Why PostgreSQL Rejects the Update
PostgreSQL cannot safely apply Transaction 2's update because it is based on stale data.
If PostgreSQL allowed the update:
Transaction 1:
Update happens from the value of 10 - 7
Transaction 2:
Update happens from the value of 1010 - 5
Final result:
5
The change made by Transaction 1 would effectively disappear.
This is known as a lost update problem.
To prevent this, PostgreSQL aborts Transaction 2 and raises the following:
ERROR: could not serialize access due to concurrent update
What Is Happening Internally
To understand the error, we need to look at PostgreSQL's MVCC architecture.
MVCC stands for:
Multi-Version Concurrency Control
Instead of modifying rows in place, PostgreSQL creates a new row version whenever an update occurs.
For example:
Initial row:
quantity = 10
Transaction 1 updates:
Version 1 - quantity = 10
Version 2 - quantity = 7
Transaction 2's snapshot still points to:
Version 1
while the database now considers the following:
Version 2
to be the latest committed version.
When Transaction 2 attempts to update Version 1, PostgreSQL realizes:
The row seen by Transaction 2 is no longer the current row.
Therefore, PostgreSQL aborts the transaction.
Why This Happens Only in Certain Isolation Levels
READ COMMITTED
In READ COMMITTED, every statement gets a fresh snapshot.
Example:
BEGIN;
SELECT quantity;
Result:
10
Transaction 1 commits:
7
Transaction 2 executes another statement:
SELECT quantity;
Result:
7
Since Transaction 2 sees the latest committed version, PostgreSQL can continue safely.
REPEATABLE READ
In REPEATABLE READ, a transaction always sees the same snapshot throughout its lifetime.
Snapshot taken at BEGIN
Even if another transaction commits later:
Transaction still sees old data
This can lead to conflicts when updates are attempted.
SERIALIZABLE
SERIALIZABLE is even stricter.
PostgreSQL ensures that concurrent transactions behave as if they were executed one after another.
When PostgreSQL detects that serialization is impossible, it aborts one of the transactions.
Error:
ERROR: could not serialize access due to concurrent update
Why Odoo Frequently Encounters This Error
Odoo performs many concurrent operations:
- Stock updates
- Sales order confirmations
- Purchase order processing
- Inventory adjustments
- Accounting transactions
- Scheduler jobs
Many users or background workers may attempt to update the same records simultaneously.
When Odoo uses transactions operating under stricter consistency guarantees, PostgreSQL may detect conflicting updates and reject one transaction.
Common examples include:
Multiple users validating stock moves
Two workers are updating the same product quantity
Concurrent modifications of accounting entries
Simultaneous scheduler executions
PostgreSQL Is Not Failing
A common misunderstanding is:
PostgreSQL failed to process the update.
This is incorrect.
What actually happened:
PostgreSQL detected a consistency violation and prevented data corruption.
The error is evidence that PostgreSQL is protecting the database.
The concurrent update error is not a bug. It is PostgreSQL's mechanism for preserving transactional consistency.
The error occurs because:
- Two transactions access the same row.
- Both read an older version of the row.
- One transaction commits first.
- The second transaction attempts to update using stale data.
- PostgreSQL detects the conflict.
- PostgreSQL aborts the second transaction to prevent a lost update.
Under REPEATABLE READ and SERIALIZABLE, PostgreSQL prioritizes consistency over blindly accepting updates. The "could not serialize access due to concurrent update" error is therefore a safety mechanism that ensures the database remains correct even under heavy concurrency.
Understanding this behavior is essential for developers working with PostgreSQL-based applications such as Odoo, where concurrent transactions are a normal part of daily operation.